Skip to content

Commit

Permalink
Improve performance of spliceChangesIntoString (#312)
Browse files Browse the repository at this point in the history
* Improve performance of spliceChangesIntoString

* Add test

* Add benchmark

* Optimize implementation a bit

* Tweak benchmarks

* Update changelog

---------

Co-authored-by: ABuffSeagull <[email protected]>
  • Loading branch information
thecrypticace and ABuffSeagull committed Aug 15, 2024
1 parent 0368ffb commit 9844623
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 9 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet!
- Improved performance with large Svelte, Liquid, and Angular files ([#312](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/312))

## [0.6.6] - 2024-08-09

Expand Down
57 changes: 57 additions & 0 deletions src/utils.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { bench, describe } from 'vitest'
import type { StringChange } from './types'
import { spliceChangesIntoString } from './utils'

describe('spliceChangesIntoString', () => {
// 44 bytes
let strTemplate = 'the quick brown fox jumps over the lazy dog '
let changesTemplate: StringChange[] = [
{ start: 10, end: 15, before: 'brown', after: 'purple' },
{ start: 4, end: 9, before: 'quick', after: 'slow' },
]

function buildFixture(repeatCount: number, changeCount: number) {
// A large set of changes across random places in the string
let indxes = new Set(
Array.from({ length: changeCount }, (_, i) =>
Math.ceil(Math.random() * repeatCount),
),
)

let changes: StringChange[] = Array.from(indxes).flatMap((idx) => {
return changesTemplate.map((change) => ({
start: change.start + strTemplate.length * idx,
end: change.end + strTemplate.length * idx,
before: change.before,
after: change.after,
}))
})

return [strTemplate.repeat(repeatCount), changes] as const
}

let [strS, changesS] = buildFixture(5, 2)
bench('small string', () => {
spliceChangesIntoString(strS, changesS)
})

let [strM, changesM] = buildFixture(100, 5)
bench('medium string', () => {
spliceChangesIntoString(strM, changesM)
})

let [strL, changesL] = buildFixture(1_000, 50)
bench('large string', () => {
spliceChangesIntoString(strL, changesL)
})

let [strXL, changesXL] = buildFixture(100_000, 500)
bench('extra large string', () => {
spliceChangesIntoString(strXL, changesXL)
})

let [strXL2, changesXL2] = buildFixture(100_000, 5_000)
bench('extra large string (5k changes)', () => {
spliceChangesIntoString(strXL2, changesXL2)
})
})
30 changes: 30 additions & 0 deletions src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { bench, describe, test } from 'vitest'
import type { StringChange } from './types'
import { spliceChangesIntoString } from './utils'

describe('spliceChangesIntoString', () => {
test('can apply changes to a string', ({ expect }) => {
let str = 'the quick brown fox jumps over the lazy dog'
let changes: StringChange[] = [
//
{ start: 10, end: 15, before: 'brown', after: 'purple' },
]

expect(spliceChangesIntoString(str, changes)).toBe(
'the quick purple fox jumps over the lazy dog',
)
})

test('changes are applied in order', ({ expect }) => {
let str = 'the quick brown fox jumps over the lazy dog'
let changes: StringChange[] = [
//
{ start: 10, end: 15, before: 'brown', after: 'purple' },
{ start: 4, end: 9, before: 'quick', after: 'slow' },
]

expect(spliceChangesIntoString(str, changes)).toBe(
'the slow purple fox jumps over the lazy dog',
)
})
})
33 changes: 25 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,34 @@ export function visit<T extends {}, Meta extends Record<string, unknown>>(
* of the string does not break the indexes of the subsequent changes.
*/
export function spliceChangesIntoString(str: string, changes: StringChange[]) {
// Sort all changes in reverse order so we apply them from the end of the string
// to the beginning. This way, the indexes for the changes after the current one
// will still be correct after applying the current one.
// If there are no changes, return the original string
if (!changes[0]) return str

// Sort all changes in order to make it easier to apply them
changes.sort((a, b) => {
return b.end - a.end || b.start - a.start
return a.end - b.end || a.start - b.start
})

// Splice in each change to the string
for (let change of changes) {
str = str.slice(0, change.start) + change.after + str.slice(change.end)
// Append original string between each chunk, and then the chunk itself
// This is sort of a String Builder pattern, thus creating less memory pressure
let result = ''

let previous = changes[0]

result += str.slice(0, previous.start)
result += previous.after

for (let i = 1; i < changes.length; ++i) {
let change = changes[i]

result += str.slice(previous.end, change.start)
result += change.after

previous = change
}

return str
// Add leftover string from last chunk to end
result += str.slice(previous.end)

return result
}

0 comments on commit 9844623

Please sign in to comment.