Skip to content

Commit 7253330

Browse files
authored
feat: improve text parsing performance (#28)
* feat: improve text parsing performance From AI: 1. **Escape Checking**: The code rechecks all trailing backslashes in the accumulated `data` string for every character, which becomes increasingly expensive as `data` grows. 2. **String Slicing**: When an escape character is found, `data = data.slice(0, -1)` creates a new string by copying almost the entire accumulated text. 3. **Repeated String Concatenation**: Adding to `data` one character at a time with `data += char` creates a new string object each time. 4. **Regular Expression Replacements**: At the end, it uses complex regex replacements that need to scan the entire string. 5. **Repeated Delimiter Checking**: For each character, it checks against multiple delimiters with `template.startsWith()`, which involves multiple string comparisons. Our optimization approach for the `text` method focused on several improvements: 1. **Simplified Escape Handling**: - Instead of recounting backslashes for every character, we now use a boolean flag `isEscaped` to track whether the current character is escaped - When we encounter a backslash, we toggle the escape state and skip adding it to the data string directly - This eliminates the expensive backslash counting operation that grew in cost with the size of the text 2. **Eliminated String Slicing**: - We no longer use `data.slice(0, -1)` which was creating a new string by copying almost the entire accumulated text - Instead, we simply don't add the backslash to the data string at all 3. **More Efficient Processing Flow**: - The code now has a clearer path for handling escaped characters - We continue to the next iteration immediately after processing an escape sequence, reducing unnecessary checks 4. **Simplified Data Processing**: - We removed the complex regular expression replacements at the end - Since we're already handling escape sequences correctly during parsing, we don't need additional processing * feat: expose parser API (#29) Parsing is an expensive computation, we now expose it to the client and accept the ast as an argument in both scan method and Chain constructor, so that clients can optimize performance. * chore: add CD
1 parent c9b339e commit 7253330

File tree

12 files changed

+152
-52
lines changed

12 files changed

+152
-52
lines changed

.github/workflows/publish.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Publish
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
publish:
10+
name: Build and Publish
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v4
16+
17+
- name: Setup Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: '22'
21+
registry-url: 'https://registry.npmjs.org'
22+
23+
- name: Install pnpm
24+
uses: pnpm/action-setup@v2
25+
with:
26+
version: 9
27+
28+
- name: Get pnpm store directory
29+
shell: bash
30+
run: |
31+
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
32+
33+
- name: Setup pnpm cache
34+
uses: actions/cache@v3
35+
with:
36+
path: ${{ env.STORE_PATH }}
37+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
38+
restore-keys: |
39+
${{ runner.os }}-pnpm-store-
40+
41+
- name: Get package version
42+
id: get_version
43+
run: |
44+
CURRENT_VERSION=$(node -p "require('./package.json').version")
45+
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
46+
47+
- name: Check version on npm
48+
id: check_version
49+
run: |
50+
NPM_VERSION=$(npm view promptl-ai version 2>/dev/null || echo "0.0.0")
51+
if [ "${{ steps.get_version.outputs.version }}" != "$NPM_VERSION" ]; then
52+
echo "should_publish=true" >> $GITHUB_OUTPUT
53+
else
54+
echo "should_publish=false" >> $GITHUB_OUTPUT
55+
fi
56+
57+
- name: Install dependencies
58+
if: steps.check_version.outputs.should_publish == 'true'
59+
run: pnpm install
60+
61+
- name: Build package (with workspace dependencies)
62+
if: steps.check_version.outputs.should_publish == 'true'
63+
run: pnpm run build
64+
65+
- name: Run linter
66+
if: steps.check_version.outputs.should_publish == 'true'
67+
run: pnpm run lint
68+
69+
- name: Run typescript checker
70+
if: steps.check_version.outputs.should_publish == 'true'
71+
run: pnpm run tc
72+
73+
- name: Run tests
74+
if: steps.check_version.outputs.should_publish == 'true'
75+
run: pnpm run test
76+
77+
- name: Publish to npm
78+
if: steps.check_version.outputs.should_publish == 'true'
79+
run: pnpm publish --access public --no-git-checks
80+
81+
env:
82+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "promptl-ai",
3-
"version": "0.6.3",
3+
"version": "0.6.4",
44
"author": "Latitude Data",
55
"license": "MIT",
66
"description": "Compiler for PromptL, the prompt language",
@@ -20,9 +20,7 @@
2020
}
2121
}
2222
},
23-
"files": [
24-
"dist"
25-
],
23+
"files": ["dist"],
2624
"scripts": {
2725
"dev": "rollup -c -w",
2826
"build": "rollup -c",

rollup.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export default [
5454
'yaml',
5555
'crypto',
5656
'zod',
57+
'fast-sha256',
5758
],
5859
},
5960
{

src/compiler/base/nodes/tags/ref.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Scope from '$promptl/compiler/scope'
22
import errors from '$promptl/error/errors'
3-
import parse from '$promptl/parser'
3+
import { parse } from '$promptl/parser'
44
import { Fragment, ReferenceTag } from '$promptl/parser/interfaces'
55

66
import { CompileNodeContext, TemplateNodeWithStatus } from '../../types'

src/compiler/chain.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
SerializedProps,
44
} from '$promptl/compiler/deserializeChain'
55
import { CHAIN_STEP_ISOLATED_ATTR } from '$promptl/constants'
6-
import parse from '$promptl/parser'
6+
import { parse } from '$promptl/parser'
77
import { Fragment } from '$promptl/parser/interfaces'
88
import {
99
AdapterMessageType,
@@ -41,15 +41,14 @@ type BuildStepResponseContent = {
4141
export class Chain<M extends AdapterMessageType = Message> {
4242
public rawText: string
4343

44-
private compileOptions: CompileOptions
45-
private ast: Fragment
46-
private scope: Scope
47-
private didStart: boolean = false
4844
private _completed: boolean = false
49-
5045
private adapter: ProviderAdapter<M>
51-
private globalMessages: Message[] = []
46+
private ast: Fragment
47+
private compileOptions: CompileOptions
48+
private didStart: boolean = false
5249
private globalConfig: Config | undefined
50+
private globalMessages: Message[] = []
51+
private scope: Scope
5352
private wasLastStepIsolated: boolean = false
5453

5554
static deserialize(args: SerializedProps) {
@@ -67,24 +66,21 @@ export class Chain<M extends AdapterMessageType = Message> {
6766
parameters?: Record<string, unknown>
6867
adapter?: ProviderAdapter<M>
6968
serialized?: {
70-
ast: Fragment
71-
scope: Scope
72-
didStart: boolean
73-
completed: boolean
74-
globalConfig: Config | undefined
75-
globalMessages: Message[]
69+
ast?: Fragment
70+
scope?: Scope
71+
didStart?: boolean
72+
completed?: boolean
73+
globalConfig?: Config
74+
globalMessages?: Message[]
7675
}
7776
} & CompileOptions) {
7877
this.rawText = prompt
79-
80-
// Init from a serialized chain
8178
this.ast = serialized?.ast ?? parse(prompt)
8279
this.scope = serialized?.scope ?? new Scope(parameters)
8380
this.didStart = serialized?.didStart ?? false
8481
this._completed = serialized?.completed ?? false
8582
this.globalConfig = serialized?.globalConfig
8683
this.globalMessages = serialized?.globalMessages ?? []
87-
8884
this.adapter = adapter
8985
this.compileOptions = compileOptions
9086

src/compiler/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { z } from 'zod'
1010
import { Chain } from './chain'
1111
import { Scan } from './scan'
1212
import type { CompileOptions, Document, ReferencePromptFn } from './types'
13+
import { Fragment } from '$promptl/parser/interfaces'
1314

1415
export async function render<M extends AdapterMessageType = Message>({
1516
prompt,
@@ -39,13 +40,15 @@ export function createChain({
3940

4041
export function scan({
4142
prompt,
43+
serialized,
4244
fullPath,
4345
referenceFn,
4446
withParameters,
4547
configSchema,
4648
requireConfig,
4749
}: {
4850
prompt: string
51+
serialized?: Fragment
4952
fullPath?: string
5053
referenceFn?: ReferencePromptFn
5154
withParameters?: string[]
@@ -54,6 +57,7 @@ export function scan({
5457
}): Promise<ConversationMetadata> {
5558
return new Scan({
5659
document: { path: fullPath ?? '', content: prompt },
60+
serialized,
5761
referenceFn,
5862
withParameters,
5963
configSchema,

src/compiler/scan.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '$promptl/constants'
99
import CompileError, { error } from '$promptl/error/error'
1010
import errors from '$promptl/error/errors'
11-
import parse from '$promptl/parser/index'
11+
import { parse } from '$promptl/parser/index'
1212
import type {
1313
Attribute,
1414
BaseNode,
@@ -70,20 +70,24 @@ export class Scan {
7070
private references: { [from: string]: string[] } = {}
7171
private referencedHashes: string[] = []
7272
private referenceDepth: number = 0
73+
private serialized?: Fragment
7374

7475
constructor({
7576
document,
7677
referenceFn,
7778
withParameters,
7879
configSchema,
7980
requireConfig,
81+
serialized,
8082
}: {
8183
document: Document
8284
referenceFn?: ReferencePromptFn
8385
withParameters?: string[]
8486
configSchema?: z.ZodType
8587
requireConfig?: boolean
88+
serialized?: Fragment
8689
}) {
90+
this.serialized = serialized
8791
this.rawText = document.content
8892
this.referenceFn = referenceFn
8993
this.fullPath = document.path
@@ -107,7 +111,7 @@ export class Scan {
107111
let fragment: Fragment
108112

109113
try {
110-
fragment = parse(this.rawText)
114+
fragment = this.serialized ?? parse(this.rawText)
111115
} catch (e) {
112116
const parseError = e as CompileError
113117
if (parseError instanceof CompileError) {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './types'
22
export * from './compiler'
3+
export * from './parser'
34
export * from './providers'
45

56
export { default as CompileError } from './error/error'

src/parser/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { BaseNode, Fragment } from './interfaces'
77
import fragment from './state/fragment'
88
import fullCharCodeAt from './utils/full_char_code_at'
99

10-
export default function parse(template: string) {
10+
export function parse(template: string) {
1111
return new Parser(template).parse()
1212
}
1313

src/parser/parser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import CompileError from '$promptl/error/error'
33
import { getExpectedError } from '$promptl/test/helpers'
44
import { describe, expect, it } from 'vitest'
55

6-
import parse from '.'
6+
import { parse } from '.'
77
import { TemplateNode } from './interfaces'
88

99
describe('Fragment', async () => {

0 commit comments

Comments
 (0)