2024-12-11 15:27:20 +01:00
|
|
|
|
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
|
|
|
|
|
|
import * as env from './env'
|
|
|
|
|
|
|
Ensure `Symbol.dispose` and `Symbol.asyncDispose` are available (#15404)
We recently introduced some better instrumentation
(https://github.com/tailwindlabs/tailwindcss/pull/15303) which uses the
new `using` keyword. I made sure that this was compiled correctly for
environments where `using` is not available yet.
The issue is that this also relies on `Symbol.dispose` being available.
In my testing on our minimal required Node.js version (18) it did work
fine. However, turns out that I was using `18.20.x` locally where
`Symbol.dispose` **_is_** available, but on older version of Node.js 18
(e.g.: `18.17.x`) it is **_not_** available. This now results in some
completely broken builds, e.g.: when running on Cloudflare Pages. See:
#15399
I could reproduce this error in CI, by temporarily downgrading the used
Node.js version to `18.17.0`. See:
<img width="1142" alt="image"
src="https://github.com/user-attachments/assets/5bf30f80-9ca0-40d9-ad02-d1ffb4e0e5dd"
/>
Implementing the proper polyfill, as recommended by the TypeScript docs
( see:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#:~:text=Symbol.dispose,-??=%20Symbol(%22Symbol.dispose
), the error goes away. (If you look at CI after the polyfill, it still
fails but for different reasons unrelated to this change)
Fixes: #15399
---
## Test plan
1. I reproduced it in CI, and I kept the commits so that you can take a
look where it fails with the `Object not disposable`.
2. Using the provided reproduction from #15399:
### Before
It works on Node.js v18.20.x, but switching to Node.js v18.17.x you can
see it fail:
<img width="1607" alt="image"
src="https://github.com/user-attachments/assets/cb6ab73a-8eb2-4003-bab7-b2390f1c879d"
/>
### After
Using pnpm's overrides, we can apply the fix from this PR and test it in
the reproduction. You'll notice that it now works in both Node.js
v18.20.x and v18.17.x
<img width="1604" alt="image"
src="https://github.com/user-attachments/assets/b3a65557-0658-4cb0-a2f9-e3079c7936d5"
/>
2024-12-16 14:17:44 +01:00
|
|
|
|
// See: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#:~:text=Symbol.dispose,-??=%20Symbol(%22Symbol.dispose
|
|
|
|
|
|
// @ts-expect-error — Ensure Symbol.dispose exists
|
|
|
|
|
|
Symbol.dispose ??= Symbol('Symbol.dispose')
|
|
|
|
|
|
// @ts-expect-error — Ensure Symbol.asyncDispose exists
|
|
|
|
|
|
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose')
|
|
|
|
|
|
|
2024-12-11 15:27:20 +01:00
|
|
|
|
export class Instrumentation implements Disposable {
|
|
|
|
|
|
#hits = new DefaultMap(() => ({ value: 0 }))
|
|
|
|
|
|
#timers = new DefaultMap(() => ({ value: 0n }))
|
|
|
|
|
|
#timerStack: { id: string; label: string; namespace: string; value: bigint }[] = []
|
|
|
|
|
|
|
Ensure `Symbol.dispose` and `Symbol.asyncDispose` are available (#15404)
We recently introduced some better instrumentation
(https://github.com/tailwindlabs/tailwindcss/pull/15303) which uses the
new `using` keyword. I made sure that this was compiled correctly for
environments where `using` is not available yet.
The issue is that this also relies on `Symbol.dispose` being available.
In my testing on our minimal required Node.js version (18) it did work
fine. However, turns out that I was using `18.20.x` locally where
`Symbol.dispose` **_is_** available, but on older version of Node.js 18
(e.g.: `18.17.x`) it is **_not_** available. This now results in some
completely broken builds, e.g.: when running on Cloudflare Pages. See:
#15399
I could reproduce this error in CI, by temporarily downgrading the used
Node.js version to `18.17.0`. See:
<img width="1142" alt="image"
src="https://github.com/user-attachments/assets/5bf30f80-9ca0-40d9-ad02-d1ffb4e0e5dd"
/>
Implementing the proper polyfill, as recommended by the TypeScript docs
( see:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#:~:text=Symbol.dispose,-??=%20Symbol(%22Symbol.dispose
), the error goes away. (If you look at CI after the polyfill, it still
fails but for different reasons unrelated to this change)
Fixes: #15399
---
## Test plan
1. I reproduced it in CI, and I kept the commits so that you can take a
look where it fails with the `Object not disposable`.
2. Using the provided reproduction from #15399:
### Before
It works on Node.js v18.20.x, but switching to Node.js v18.17.x you can
see it fail:
<img width="1607" alt="image"
src="https://github.com/user-attachments/assets/cb6ab73a-8eb2-4003-bab7-b2390f1c879d"
/>
### After
Using pnpm's overrides, we can apply the fix from this PR and test it in
the reproduction. You'll notice that it now works in both Node.js
v18.20.x and v18.17.x
<img width="1604" alt="image"
src="https://github.com/user-attachments/assets/b3a65557-0658-4cb0-a2f9-e3079c7936d5"
/>
2024-12-16 14:17:44 +01:00
|
|
|
|
constructor(
|
|
|
|
|
|
private defaultFlush = (message: string) => void process.stderr.write(`${message}\n`),
|
|
|
|
|
|
) {}
|
2024-12-11 15:27:20 +01:00
|
|
|
|
|
|
|
|
|
|
hit(label: string) {
|
|
|
|
|
|
this.#hits.get(label).value++
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
start(label: string) {
|
|
|
|
|
|
let namespace = this.#timerStack.map((t) => t.label).join('//')
|
|
|
|
|
|
let id = `${namespace}${namespace.length === 0 ? '' : '//'}${label}`
|
|
|
|
|
|
|
|
|
|
|
|
this.#hits.get(id).value++
|
|
|
|
|
|
|
|
|
|
|
|
// Create the timer if it doesn't exist yet
|
|
|
|
|
|
this.#timers.get(id)
|
|
|
|
|
|
|
|
|
|
|
|
this.#timerStack.push({ id, label, namespace, value: process.hrtime.bigint() })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
end(label: string) {
|
|
|
|
|
|
let end = process.hrtime.bigint()
|
|
|
|
|
|
|
|
|
|
|
|
if (this.#timerStack[this.#timerStack.length - 1].label !== label) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Mismatched timer label: \`${label}\`, expected \`${
|
|
|
|
|
|
this.#timerStack[this.#timerStack.length - 1].label
|
|
|
|
|
|
}\``,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let parent = this.#timerStack.pop()!
|
|
|
|
|
|
let elapsed = end - parent.value
|
|
|
|
|
|
this.#timers.get(parent.id).value += elapsed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
|
|
this.#hits.clear()
|
|
|
|
|
|
this.#timers.clear()
|
|
|
|
|
|
this.#timerStack.splice(0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
report(flush = this.defaultFlush) {
|
|
|
|
|
|
let output: string[] = []
|
|
|
|
|
|
let hasHits = false
|
|
|
|
|
|
|
|
|
|
|
|
// Auto end any pending timers
|
|
|
|
|
|
for (let i = this.#timerStack.length - 1; i >= 0; i--) {
|
|
|
|
|
|
this.end(this.#timerStack[i].label)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (let [label, { value: count }] of this.#hits.entries()) {
|
|
|
|
|
|
if (this.#timers.has(label)) continue
|
|
|
|
|
|
if (output.length === 0) {
|
|
|
|
|
|
hasHits = true
|
|
|
|
|
|
output.push('Hits:')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let depth = label.split('//').length
|
|
|
|
|
|
output.push(`${' '.repeat(depth)}${label} ${dim(blue(`× ${count}`))}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.#timers.size > 0 && hasHits) {
|
|
|
|
|
|
output.push('\nTimers:')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let max = -Infinity
|
|
|
|
|
|
let computed = new Map<string, string>()
|
|
|
|
|
|
for (let [label, { value }] of this.#timers) {
|
|
|
|
|
|
let x = `${(Number(value) / 1e6).toFixed(2)}ms`
|
|
|
|
|
|
computed.set(label, x)
|
|
|
|
|
|
max = Math.max(max, x.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (let label of this.#timers.keys()) {
|
|
|
|
|
|
let depth = label.split('//').length
|
|
|
|
|
|
output.push(
|
|
|
|
|
|
`${dim(`[${computed.get(label)!.padStart(max, ' ')}]`)}${' '.repeat(depth - 1)}${depth === 1 ? ' ' : dim(' ↳ ')}${label.split('//').pop()} ${
|
|
|
|
|
|
this.#hits.get(label).value === 1 ? '' : dim(blue(`× ${this.#hits.get(label).value}`))
|
|
|
|
|
|
}`.trimEnd(),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
flush(`\n${output.join('\n')}\n`)
|
|
|
|
|
|
this.reset()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Symbol.dispose]() {
|
|
|
|
|
|
env.DEBUG && this.report()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function dim(input: string) {
|
|
|
|
|
|
return `\u001b[2m${input}\u001b[22m`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function blue(input: string) {
|
|
|
|
|
|
return `\u001b[34m${input}\u001b[39m`
|
|
|
|
|
|
}
|