Skip to content

Commit 4189628

Browse files
authored
perf(core): token split performance improvements (#1301)
1 parent c5d310b commit 4189628

3 files changed

Lines changed: 86 additions & 6 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { DecorationItem } from 'shiki'
2+
import { createHighlighter, createOnigurumaEngine } from 'shiki'
3+
import { afterAll, bench, describe } from 'vitest'
4+
5+
const lineCount = 5_000
6+
const code = Array.from(
7+
{ length: lineCount },
8+
(_, line) => `const value${line.toString().padStart(5, '0')} = compute(${line}, 'before');`,
9+
).join('\n')
10+
const decorations: DecorationItem[] = Array.from(
11+
{ length: lineCount },
12+
(_, line) => ({
13+
start: { line, character: 8 },
14+
end: { line, character: 13 },
15+
properties: { class: 'highlighted' },
16+
}),
17+
)
18+
19+
const engine = await createOnigurumaEngine(() => import('shiki/wasm'))
20+
const highlighter = await createHighlighter({
21+
engine,
22+
langs: ['typescript'],
23+
themes: ['vitesse-dark'],
24+
})
25+
26+
afterAll(() => highlighter.dispose())
27+
28+
describe(`single file (${lineCount} lines and decorations)`, () => {
29+
bench('default decoration rendering', () => {
30+
highlighter.codeToHast(code, {
31+
decorations,
32+
lang: 'typescript',
33+
theme: 'vitesse-dark',
34+
})
35+
}, {
36+
iterations: 3,
37+
time: 0,
38+
warmupIterations: 1,
39+
warmupTime: 0,
40+
})
41+
})

‎packages/core/src/utils/tokens.ts‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ export function splitToken<
3939
return tokens
4040
}
4141

42+
// Find the first candidate so each token only scans its contained breakpoints.
43+
function findFirstBreakpointAfter(breakpoints: number[], offset: number): number {
44+
let start = 0
45+
let end = breakpoints.length
46+
47+
while (start < end) {
48+
const middle = start + ((end - start) >> 1)
49+
if (breakpoints[middle] <= offset)
50+
start = middle + 1
51+
else
52+
end = middle
53+
}
54+
55+
return start
56+
}
57+
4258
/**
4359
* Split 2D tokens array by given breakpoints.
4460
*/
@@ -55,14 +71,20 @@ export function splitTokens<
5571

5672
return tokens.map((line) => {
5773
return line.flatMap((token) => {
58-
const breakpointsInToken = sorted
59-
.filter(i => token.offset < i && i < token.offset + token.content.length)
60-
.map(i => i - token.offset)
61-
.sort((a, b) => a - b)
74+
const tokenEnd = token.offset + token.content.length
75+
const start = findFirstBreakpointAfter(sorted, token.offset)
76+
let end = start
77+
78+
while (end < sorted.length && sorted[end] < tokenEnd)
79+
end++
6280

63-
if (!breakpointsInToken.length)
81+
if (start === end)
6482
return token
6583

84+
const breakpointsInToken = sorted.slice(start, end)
85+
for (let index = 0; index < breakpointsInToken.length; index++)
86+
breakpointsInToken[index] -= token.offset
87+
6688
return splitToken(token, breakpointsInToken)
6789
})
6890
})

‎packages/core/test/tokens.test.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
import { createJavaScriptRegexEngine } from 'shiki'
22
import { describe, expect, it } from 'vitest'
3-
import { codeToHtml, codeToTokens, codeToTokensBase, createShikiPrimitiveAsync } from '../src'
3+
import { codeToHtml, codeToTokens, codeToTokensBase, createShikiPrimitiveAsync, splitTokens } from '../src'
4+
5+
describe('splitTokens', () => {
6+
it('splits at contained breakpoints while preserving token order and fields', () => {
7+
const tokens = [[
8+
{ content: 'wxyz', marker: 'later', offset: 10 },
9+
{ content: 'abcd', marker: 'earlier', offset: 0 },
10+
]]
11+
12+
expect(splitTokens(tokens, [14, 2, 13, 11, 10, 4, 0, 2])).toEqual([[
13+
{ content: 'w', marker: 'later', offset: 10 },
14+
{ content: 'xy', marker: 'later', offset: 11 },
15+
{ content: 'z', marker: 'later', offset: 13 },
16+
{ content: 'ab', marker: 'earlier', offset: 0 },
17+
{ content: 'cd', marker: 'earlier', offset: 2 },
18+
]])
19+
})
20+
})
421

522
it('includeExplanation', async () => {
623
using engine = await createShikiPrimitiveAsync({

0 commit comments

Comments
 (0)