Speed up segmentation (#13415)

* Speed up segmentation

* Add segment benchmark

* Add and move comments

* Update

* Tweak comments

* Tweak variable name

* Tweak

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
This commit is contained in:
ottomated 2024-04-02 13:43:37 -07:00 • committed by GitHub
parent be94e21136
commit 5db92a4ceb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 23 deletions

View file

@ -0,0 +1,14 @@
import { bench } from 'vitest'
import { segment } from './segment'
const values = [
['hover:focus:underline', ':'],
['var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0)', ','],
['var(--some-value,env(safe-area-inset-top,var(--some-other-value,env(safe-area-inset))))', ','],
]
bench('segment', () => {
for (let [value, sep] of values) {
segment(value, sep)
}
})

View file

@ -1,8 +1,23 @@
// This is a shared buffer that is used to keep track of the current nesting level
// of parens, brackets, and braces. It is used to determine if a character is at
// the top-level of a string. This is a performance optimization to avoid memory
// allocations on every call to `segment`.
const closingBracketStack = new Uint8Array(256)
// All numbers are equivalent to the value returned by `String#charCodeAt(0)`
const BACKSLASH = 0x5c
const OPEN_PAREN = 0x28
const OPEN_BRACKET = 0x5b
const OPEN_CURLY = 0x7b
const CLOSE_PAREN = 0x29
const CLOSE_BRACKET = 0x5d
const CLOSE_CURLY = 0x7d
/**
* This splits a string on a top-level character.
*
* Regex doesn't support recursion (at least not the JS-flavored version).
* So we have to use a tiny state machine to keep track of paren placement.
* Regex doesn't support recursion (at least not the JS-flavored version),
* so we have to use a tiny state machine to keep track of paren placement.
*
* Expected behavior using commas:
* var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0)
@ -11,43 +26,50 @@
* ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens
*/
export function segment(input: string, separator: string) {
// Stack of characters to close open brackets. Appending to a string because
// it's faster than an array of strings.
let closingBracketStack = ''
// SAFETY: We can use an index into a shared buffer because this function is
// synchronous, non-recursive, and runs in a single-threaded envionment.
let stackPos = 0
let parts: string[] = []
let lastPos = 0
for (let idx = 0; idx < input.length; idx++) {
let char = input[idx]
let separatorCode = separator.charCodeAt(0)
if (closingBracketStack.length === 0 && char === separator) {
for (let idx = 0; idx < input.length; idx++) {
let char = input.charCodeAt(idx)
if (stackPos === 0 && char === separatorCode) {
parts.push(input.slice(lastPos, idx))
lastPos = idx + 1
continue
}
switch (char) {
case '\\':
case BACKSLASH:
// The next character is escaped, so we skip it.
idx += 1
break
case '(':
closingBracketStack += ')'
case OPEN_PAREN:
closingBracketStack[stackPos] = CLOSE_PAREN
stackPos++
break
case '[':
closingBracketStack += ']'
case OPEN_BRACKET:
closingBracketStack[stackPos] = CLOSE_BRACKET
stackPos++
break
case '{':
closingBracketStack += '}'
case OPEN_CURLY:
closingBracketStack[stackPos] = CLOSE_CURLY
stackPos++
break
case ')':
case ']':
case '}':
if (
closingBracketStack.length > 0 &&
char === closingBracketStack[closingBracketStack.length - 1]
) {
closingBracketStack = closingBracketStack.slice(0, closingBracketStack.length - 1)
case CLOSE_BRACKET:
case CLOSE_CURLY:
case CLOSE_PAREN:
if (stackPos > 0 && char === closingBracketStack[stackPos - 1]) {
// SAFETY: The buffer does not need to be mutated because the stack is
// only ever read from or written to its current position. Its current
// position is only ever incremented after writing to it. Meaning that
// the buffer can be dirty for the next use and still be correct since
// reading/writing always starts at position `0`.
stackPos--
}
break
}