Skip to content

Commit bcf7099

Browse files
authored
Improve debug logs (#15303)
This PR improves the debug logs for the `@tailwindcss/postcss` integration. It uses custom instrumentation to provide a nested but detailed overview of where time is spent during the build process. The updated logs look like this: ``` [0.15ms] [@tailwindcss/postcss] src/app/geistsans_9fc57718.module.css [0.14ms] ↳ Quick bail check [0.02ms] [@tailwindcss/postcss] src/app/geistsans_9fc57718.module.css [0.01ms] ↳ Quick bail check [0.03ms] [@tailwindcss/postcss] src/app/geistmono_b9f59162.module.css [0.02ms] ↳ Quick bail check [0.12ms] [@tailwindcss/postcss] src/app/geistmono_b9f59162.module.css [0.11ms] ↳ Quick bail check [42.09ms] [@tailwindcss/postcss] src/app/globals.css [ 0.01ms] ↳ Quick bail check [12.12ms] ↳ Setup compiler [ 0.11ms] ↳ PostCSS AST -> Tailwind CSS AST [11.99ms] ↳ Create compiler [ 0.07ms] ↳ Register full rebuild paths [ 0.06ms] ↳ Setup scanner [ 7.51ms] ↳ Scan for candidates [ 5.86ms] ↳ Register dependency messages [ 5.88ms] ↳ Build utilities [ 8.34ms] ↳ Optimization [ 0.23ms] ↳ AST -> CSS [ 4.20ms] ↳ Lightning CSS [ 3.89ms] ↳ CSS -> PostCSS AST [ 1.97ms] ↳ Update PostCSS AST ```
1 parent 2cbc915 commit bcf7099

File tree

10 files changed

+301
-77
lines changed

10 files changed

+301
-77
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Skip creating a compiler for CSS files that should not be processed ([#15340](https://github.com/tailwindlabs/tailwindcss/pull/15340))
1818
- Fix missing `shadow-none` suggestion in IntelliSense ([#15342](https://github.com/tailwindlabs/tailwindcss/pull/15342))
1919
- Optimize AST before printing for IntelliSense ([#15347](https://github.com/tailwindlabs/tailwindcss/pull/15347))
20+
- Improve debug logs to get better insights ([#15303](https://github.com/tailwindlabs/tailwindcss/pull/15303))
2021

2122
### Changed
2223

crates/oxide/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ pub mod paths;
2424
pub mod scanner;
2525

2626
static SHOULD_TRACE: sync::LazyLock<bool> = sync::LazyLock::new(
27-
|| matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || value.eq("1") || value.eq("true") || value.contains("tailwind")),
27+
|| matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
2828
);
2929

3030
fn init_tracing() {

packages/@tailwindcss-cli/src/commands/build/index.ts

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import watcher from '@parcel/watcher'
2-
import { compile, env } from '@tailwindcss/node'
2+
import { compile, env, Instrumentation } from '@tailwindcss/node'
33
import { clearRequireCache } from '@tailwindcss/node/require-cache'
44
import { Scanner, type ChangedContent } from '@tailwindcss/oxide'
55
import { Features, transform } from 'lightningcss'
@@ -19,6 +19,7 @@ import {
1919
import { drainStdin, outputFile } from './utils'
2020

2121
const css = String.raw
22+
const DEBUG = env.DEBUG
2223

2324
export function options() {
2425
return {
@@ -66,6 +67,9 @@ async function handleError<T>(fn: () => T): Promise<T> {
6667
}
6768

6869
export async function handle(args: Result<ReturnType<typeof options>>) {
70+
using I = new Instrumentation()
71+
DEBUG && I.start('[@tailwindcss/cli] (initial build)')
72+
6973
let base = path.resolve(args['--cwd'])
7074

7175
// Resolve the output as an absolute path.
@@ -103,18 +107,18 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
103107
optimizedCss: '',
104108
}
105109

106-
async function write(css: string, args: Result<ReturnType<typeof options>>) {
110+
async function write(css: string, args: Result<ReturnType<typeof options>>, I: Instrumentation) {
107111
let output = css
108112

109113
// Optimize the output
110114
if (args['--minify'] || args['--optimize']) {
111115
if (css !== previous.css) {
112-
env.DEBUG && console.time('[@tailwindcss/cli] Optimize CSS')
116+
DEBUG && I.start('Optimize CSS')
113117
let optimizedCss = optimizeCss(css, {
114118
file: args['--input'] ?? 'input.css',
115119
minify: args['--minify'] ?? false,
116120
})
117-
env.DEBUG && console.timeEnd('[@tailwindcss/cli] Optimize CSS')
121+
DEBUG && I.end('Optimize CSS')
118122
previous.css = css
119123
previous.optimizedCss = optimizedCss
120124
output = optimizedCss
@@ -124,13 +128,13 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
124128
}
125129

126130
// Write the output
127-
env.DEBUG && console.time('[@tailwindcss/cli] Write output')
131+
DEBUG && I.start('Write output')
128132
if (args['--output']) {
129133
await outputFile(args['--output'], output)
130134
} else {
131135
println(output)
132136
}
133-
env.DEBUG && console.timeEnd('[@tailwindcss/cli] Write output')
137+
DEBUG && I.end('Write output')
134138
}
135139

136140
let inputFilePath =
@@ -140,8 +144,8 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
140144

141145
let fullRebuildPaths: string[] = inputFilePath ? [inputFilePath] : []
142146

143-
async function createCompiler(css: string) {
144-
env.DEBUG && console.time('[@tailwindcss/cli] Setup compiler')
147+
async function createCompiler(css: string, I: Instrumentation) {
148+
DEBUG && I.start('Setup compiler')
145149
let compiler = await compile(css, {
146150
base: inputBasePath,
147151
onDependency(path) {
@@ -165,12 +169,12 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
165169
})().concat(compiler.globs)
166170

167171
let scanner = new Scanner({ sources })
168-
env.DEBUG && console.timeEnd('[@tailwindcss/cli] Setup compiler')
172+
DEBUG && I.end('Setup compiler')
169173

170174
return [compiler, scanner] as const
171175
}
172176

173-
let [compiler, scanner] = await handleError(() => createCompiler(input))
177+
let [compiler, scanner] = await handleError(() => createCompiler(input, I))
174178

175179
// Watch for changes
176180
if (args['--watch']) {
@@ -182,6 +186,12 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
182186
// trigger a rebuild because that will result in an infinite loop.
183187
if (files.length === 1 && files[0] === args['--output']) return
184188

189+
using I = new Instrumentation()
190+
DEBUG && I.start('[@tailwindcss/cli] (watcher)')
191+
192+
// Re-compile the input
193+
let start = process.hrtime.bigint()
194+
185195
let changedFiles: ChangedContent[] = []
186196
let rebuildStrategy: 'incremental' | 'full' = 'incremental'
187197

@@ -206,9 +216,6 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
206216
} satisfies ChangedContent)
207217
}
208218

209-
// Re-compile the input
210-
let start = process.hrtime.bigint()
211-
212219
// Track the compiled CSS
213220
let compiledCss = ''
214221

@@ -226,32 +233,36 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
226233
fullRebuildPaths = inputFilePath ? [inputFilePath] : []
227234

228235
// Create a new compiler, given the new `input`
229-
;[compiler, scanner] = await createCompiler(input)
236+
;[compiler, scanner] = await createCompiler(input, I)
230237

231238
// Scan the directory for candidates
232-
env.DEBUG && console.time('[@tailwindcss/cli] Scan for candidates')
239+
DEBUG && I.start('Scan for candidates')
233240
let candidates = scanner.scan()
234-
env.DEBUG && console.timeEnd('[@tailwindcss/cli] Scan for candidates')
241+
DEBUG && I.end('Scan for candidates')
235242

236243
// Setup new watchers
244+
DEBUG && I.start('Setup new watchers')
237245
let newCleanupWatchers = await createWatchers(watchDirectories(scanner), handle)
246+
DEBUG && I.end('Setup new watchers')
238247

239248
// Clear old watchers
249+
DEBUG && I.start('Cleanup old watchers')
240250
await cleanupWatchers()
251+
DEBUG && I.end('Cleanup old watchers')
241252

242253
cleanupWatchers = newCleanupWatchers
243254

244255
// Re-compile the CSS
245-
env.DEBUG && console.time('[@tailwindcss/cli] Build CSS')
256+
DEBUG && I.start('Build CSS')
246257
compiledCss = compiler.build(candidates)
247-
env.DEBUG && console.timeEnd('[@tailwindcss/cli] Build CSS')
258+
DEBUG && I.end('Build CSS')
248259
}
249260

250261
// Scan changed files only for incremental rebuilds.
251262
else if (rebuildStrategy === 'incremental') {
252-
env.DEBUG && console.time('[@tailwindcss/cli] Scan for candidates')
263+
DEBUG && I.start('Scan for candidates')
253264
let newCandidates = scanner.scanFiles(changedFiles)
254-
env.DEBUG && console.timeEnd('[@tailwindcss/cli] Scan for candidates')
265+
DEBUG && I.end('Scan for candidates')
255266

256267
// No new candidates found which means we don't need to write to
257268
// disk, and can return early.
@@ -261,12 +272,12 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
261272
return
262273
}
263274

264-
env.DEBUG && console.time('[@tailwindcss/cli] Build CSS')
275+
DEBUG && I.start('Build CSS')
265276
compiledCss = compiler.build(newCandidates)
266-
env.DEBUG && console.timeEnd('[@tailwindcss/cli] Build CSS')
277+
DEBUG && I.end('Build CSS')
267278
}
268279

269-
await write(compiledCss, args)
280+
await write(compiledCss, args, I)
270281

271282
let end = process.hrtime.bigint()
272283
eprintln(`Done in ${formatDuration(end - start)}`)
@@ -295,13 +306,13 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
295306
process.stdin.resume()
296307
}
297308

298-
env.DEBUG && console.time('[@tailwindcss/cli] Scan for candidates')
309+
DEBUG && I.start('Scan for candidates')
299310
let candidates = scanner.scan()
300-
env.DEBUG && console.timeEnd('[@tailwindcss/cli] Scan for candidates')
301-
env.DEBUG && console.time('[@tailwindcss/cli] Build CSS')
311+
DEBUG && I.end('Scan for candidates')
312+
DEBUG && I.start('Build CSS')
302313
let output = await handleError(() => compiler.build(candidates))
303-
env.DEBUG && console.timeEnd('[@tailwindcss/cli] Build CSS')
304-
await write(output, args)
314+
DEBUG && I.end('Build CSS')
315+
await write(output, args, I)
305316

306317
let end = process.hrtime.bigint()
307318
eprintln(header())

packages/@tailwindcss-node/src/index.cts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Module from 'node:module'
22
import { pathToFileURL } from 'node:url'
33
import * as env from './env'
44
export * from './compile'
5+
export * from './instrumentation'
56
export * from './normalize-path'
67
export { env }
78

packages/@tailwindcss-node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Module from 'node:module'
22
import { pathToFileURL } from 'node:url'
33
import * as env from './env'
44
export { __unstable__loadDesignSystem, compile, compileAst, Features } from './compile'
5+
export * from './instrumentation'
56
export * from './normalize-path'
67
export { env }
78

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { stripVTControlCharacters } from 'util'
2+
import { expect, it } from 'vitest'
3+
import { Instrumentation } from './instrumentation'
4+
5+
it('should add instrumentation', () => {
6+
let I = new Instrumentation()
7+
8+
I.start('Foo')
9+
let x = 1
10+
for (let i = 0; i < 100; i++) {
11+
I.start('Bar')
12+
x **= 2
13+
I.end('Bar')
14+
}
15+
I.end('Foo')
16+
17+
I.hit('Potato')
18+
I.hit('Potato')
19+
I.hit('Potato')
20+
I.hit('Potato')
21+
22+
expect.assertions(1)
23+
24+
I.report((output) => {
25+
expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(`
26+
"
27+
Hits:
28+
Potato × 4
29+
30+
Timers:
31+
[0.xxms] Foo
32+
[0.xxms] ↳ Bar × 100
33+
"
34+
`)
35+
})
36+
})
37+
38+
it('should auto end pending timers when reporting', () => {
39+
let I = new Instrumentation()
40+
41+
I.start('Foo')
42+
let x = 1
43+
for (let i = 0; i < 100; i++) {
44+
I.start('Bar')
45+
x **= 2
46+
I.end('Bar')
47+
}
48+
I.start('Baz')
49+
50+
expect.assertions(1)
51+
52+
I.report((output) => {
53+
expect(stripVTControlCharacters(output).replace(/\[.*\]/g, '[0.xxms]')).toMatchInlineSnapshot(`
54+
"
55+
[0.xxms] Foo
56+
[0.xxms] ↳ Bar × 100
57+
[0.xxms] ↳ Baz
58+
"
59+
`)
60+
})
61+
})
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
2+
import * as env from './env'
3+
4+
export class Instrumentation implements Disposable {
5+
#hits = new DefaultMap(() => ({ value: 0 }))
6+
#timers = new DefaultMap(() => ({ value: 0n }))
7+
#timerStack: { id: string; label: string; namespace: string; value: bigint }[] = []
8+
9+
constructor(private defaultFlush = (message: string) => process.stderr.write(`${message}\n`)) {}
10+
11+
hit(label: string) {
12+
this.#hits.get(label).value++
13+
}
14+
15+
start(label: string) {
16+
let namespace = this.#timerStack.map((t) => t.label).join('//')
17+
let id = `${namespace}${namespace.length === 0 ? '' : '//'}${label}`
18+
19+
this.#hits.get(id).value++
20+
21+
// Create the timer if it doesn't exist yet
22+
this.#timers.get(id)
23+
24+
this.#timerStack.push({ id, label, namespace, value: process.hrtime.bigint() })
25+
}
26+
27+
end(label: string) {
28+
let end = process.hrtime.bigint()
29+
30+
if (this.#timerStack[this.#timerStack.length - 1].label !== label) {
31+
throw new Error(
32+
`Mismatched timer label: \`${label}\`, expected \`${
33+
this.#timerStack[this.#timerStack.length - 1].label
34+
}\``,
35+
)
36+
}
37+
38+
let parent = this.#timerStack.pop()!
39+
let elapsed = end - parent.value
40+
this.#timers.get(parent.id).value += elapsed
41+
}
42+
43+
reset() {
44+
this.#hits.clear()
45+
this.#timers.clear()
46+
this.#timerStack.splice(0)
47+
}
48+
49+
report(flush = this.defaultFlush) {
50+
let output: string[] = []
51+
let hasHits = false
52+
53+
// Auto end any pending timers
54+
for (let i = this.#timerStack.length - 1; i >= 0; i--) {
55+
this.end(this.#timerStack[i].label)
56+
}
57+
58+
for (let [label, { value: count }] of this.#hits.entries()) {
59+
if (this.#timers.has(label)) continue
60+
if (output.length === 0) {
61+
hasHits = true
62+
output.push('Hits:')
63+
}
64+
65+
let depth = label.split('//').length
66+
output.push(`${' '.repeat(depth)}${label} ${dim(blue(${count}`))}`)
67+
}
68+
69+
if (this.#timers.size > 0 && hasHits) {
70+
output.push('\nTimers:')
71+
}
72+
73+
let max = -Infinity
74+
let computed = new Map<string, string>()
75+
for (let [label, { value }] of this.#timers) {
76+
let x = `${(Number(value) / 1e6).toFixed(2)}ms`
77+
computed.set(label, x)
78+
max = Math.max(max, x.length)
79+
}
80+
81+
for (let label of this.#timers.keys()) {
82+
let depth = label.split('//').length
83+
output.push(
84+
`${dim(`[${computed.get(label)!.padStart(max, ' ')}]`)}${' '.repeat(depth - 1)}${depth === 1 ? ' ' : dim(' ↳ ')}${label.split('//').pop()} ${
85+
this.#hits.get(label).value === 1 ? '' : dim(blue(${this.#hits.get(label).value}`))
86+
}`.trimEnd(),
87+
)
88+
}
89+
90+
flush(`\n${output.join('\n')}\n`)
91+
this.reset()
92+
}
93+
94+
[Symbol.dispose]() {
95+
env.DEBUG && this.report()
96+
}
97+
}
98+
99+
function dim(input: string) {
100+
return `\u001b[2m${input}\u001b[22m`
101+
}
102+
103+
function blue(input: string) {
104+
return `\u001b[34m${input}\u001b[39m`
105+
}

0 commit comments

Comments
 (0)