Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of spliceChangesIntoString #312

Merged
merged 6 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading