diff --git a/src/chart/track-hasher.ts b/src/chart/track-hasher.ts index e7cef65..73312d6 100644 --- a/src/chart/track-hasher.ts +++ b/src/chart/track-hasher.ts @@ -49,6 +49,17 @@ export function calculateTrackHash(parsedChart: ParsedChart, instrument: Instrum const uint8Array = new Uint8Array(buffer) const view = new DataView(buffer, 0) + // For a chart, every `tick` and `length` is a non-negative 32-bit integer — + // max per-tick values are well below 2^31 for any real song. So we can write + // each 8-byte little-endian int64 slot as [low 32 bits, high 32 bits = 0], + // skipping the `BigInt(x)` allocation that `setBigInt64` forces. A full + // hash on a dense track fires tens of thousands of these — the BigInt + // allocations dominate GC in the hasher path. + function writeInt64LE(offset: number, v: number): void { + view.setUint32(offset, v >>> 0, true) + view.setUint32(offset + 4, 0, true) + } + view.setUint32(0, 0x43484e46, false) // Big endian for format header, little endian for everything else view.setUint32(4, 20240320, true) view.setUint32(8, parsedChart.resolution, true) @@ -56,14 +67,14 @@ export function calculateTrackHash(parsedChart: ParsedChart, instrument: Instrum view.setUint32(i, tempoData.length, true) i += 4 for (const tempo of tempoData) { - view.setBigInt64(i, BigInt(tempo.tick), true) + writeInt64LE(i, tempo.tick) view.setFloat64(i + 8, tempo.beatsPerMinute, true) i += 16 } view.setUint32(i, timeSignatureData.length, true) i += 4 for (const timeSignature of timeSignatureData) { - view.setBigInt64(i, BigInt(timeSignature.tick), true) + writeInt64LE(i, timeSignature.tick) view.setUint32(i + 8, timeSignature.numerator, true) view.setUint32(i + 12, timeSignature.denominator, true) i += 16 @@ -71,38 +82,38 @@ export function calculateTrackHash(parsedChart: ParsedChart, instrument: Instrum view.setUint32(i, starPowerData.length, true) i += 4 for (const starPower of starPowerData) { - view.setBigInt64(i, BigInt(starPower.tick), true) - view.setBigInt64(i + 8, BigInt(starPower.length), true) + writeInt64LE(i, starPower.tick) + writeInt64LE(i + 8, starPower.length) i += 16 } view.setUint32(i, soloSectionData.length, true) i += 4 for (const soloSection of soloSectionData) { - view.setBigInt64(i, BigInt(soloSection.tick), true) - view.setBigInt64(i + 8, BigInt(soloSection.length), true) + writeInt64LE(i, soloSection.tick) + writeInt64LE(i + 8, soloSection.length) i += 16 } view.setUint32(i, flexLanesData.length, true) i += 4 for (const flexLane of flexLanesData) { - view.setBigInt64(i, BigInt(flexLane.tick), true) - view.setBigInt64(i + 8, BigInt(flexLane.length), true) + writeInt64LE(i, flexLane.tick) + writeInt64LE(i + 8, flexLane.length) view.setUint8(i + 16, flexLane.isDouble ? 1 : 0) i += 17 } view.setInt32(i, drumFreestyleSectionData.length, true) i += 4 for (const drumFreestyleSection of drumFreestyleSectionData) { - view.setBigInt64(i, BigInt(drumFreestyleSection.tick), true) - view.setBigInt64(i + 8, BigInt(drumFreestyleSection.length), true) + writeInt64LE(i, drumFreestyleSection.tick) + writeInt64LE(i + 8, drumFreestyleSection.length) view.setUint8(i + 16, drumFreestyleSection.isCoda ? 1 : 0) i += 17 } view.setInt32(i, notesData.length, true) i += 4 for (const note of notesData) { - view.setBigInt64(i, BigInt(note.tick), true) - view.setBigInt64(i + 8, BigInt(note.length), true) + writeInt64LE(i, note.tick) + writeInt64LE(i + 8, note.length) view.setUint32(i + 16, note.type, true) view.setUint32(i + 20, note.flags, true) i += 24