From 8acf6b95f46c4981bd4712f94073677d054b50bb Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:36:54 -0700 Subject: [PATCH 1/3] Report process CPU time in extended diagnostics Wall-clock timings include time spent waiting for other processes and I/O. Process CPU time provides a complementary measure of compiler work across all threads without counting time spent executing unrelated processes. --- tsc/internal/execute/build/orchestrator.go | 2 +- tsc/internal/execute/tsc/emit.go | 2 +- tsc/internal/execute/tsc/statistics.go | 8 +++++++- tsc/internal/osutil/cputime_other.go | 12 ++++++++++++ tsc/internal/osutil/cputime_unix.go | 17 +++++++++++++++++ tsc/internal/osutil/cputime_windows.go | 17 +++++++++++++++++ tsc/internal/osutil/osutil.go | 8 ++++++++ 7 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tsc/internal/osutil/cputime_other.go create mode 100644 tsc/internal/osutil/cputime_unix.go create mode 100644 tsc/internal/osutil/cputime_windows.go diff --git a/tsc/internal/execute/build/orchestrator.go b/tsc/internal/execute/build/orchestrator.go index 664e47844616e..6a1db66d7fddd 100644 --- a/tsc/internal/execute/build/orchestrator.go +++ b/tsc/internal/execute/build/orchestrator.go @@ -60,7 +60,7 @@ func (b *OrchestratorResult) reportWithFilesToDelete(o *Orchestrator, reportFile return } b.Statistics.SetTotalTime(o.opts.Sys.SinceStart()) - b.Statistics.Report(o.opts.Sys.Writer(), o.opts.Testing) + b.Statistics.Report(o.opts.Sys.Writer(), o.opts.Testing, o.opts.Command.CompilerOptions.ExtendedDiagnostics.IsTrue()) } type Orchestrator struct { diff --git a/tsc/internal/execute/tsc/emit.go b/tsc/internal/execute/tsc/emit.go index 4f30b1617ada1..44fa09179f810 100644 --- a/tsc/internal/execute/tsc/emit.go +++ b/tsc/internal/execute/tsc/emit.go @@ -60,7 +60,7 @@ func EmitAndReportStatistics(input EmitInput) (CompileAndEmitResult, *Statistics runtime.ReadMemStats(&memStats) statistics = statisticsFromProgram(input, &memStats) - statistics.Report(input.Writer, input.Testing) + statistics.Report(input.Writer, input.Testing, input.Config.CompilerOptions().ExtendedDiagnostics.IsTrue()) } if result.EmitResult.EmitSkipped && len(result.Diagnostics) > 0 { diff --git a/tsc/internal/execute/tsc/statistics.go b/tsc/internal/execute/tsc/statistics.go index ce2ac6d5d4bdc..f2b843ba88235 100644 --- a/tsc/internal/execute/tsc/statistics.go +++ b/tsc/internal/execute/tsc/statistics.go @@ -10,6 +10,7 @@ import ( "time" "github.com/microsoft/TypeScript/tsc/internal/compiler" + "github.com/microsoft/TypeScript/tsc/internal/osutil" ) type tableRow struct { @@ -83,7 +84,7 @@ func statisticsFromProgram(input EmitInput, memStats *runtime.MemStats) *Statist } } -func (s *Statistics) Report(w io.Writer, testing CommandLineTesting) { +func (s *Statistics) Report(w io.Writer, testing CommandLineTesting, extendedDiagnostics bool) { if testing != nil { testing.OnStatisticsStart(w) defer testing.OnStatisticsEnd(w) @@ -126,6 +127,11 @@ func (s *Statistics) Report(w io.Writer, testing CommandLineTesting) { } s.addContentMapperStatistics(&table, prefix) table.add(prefix+"Total time", s.compileTimes.totalTime) + if extendedDiagnostics { + if cpuTime, err := osutil.ProcessCPUTime(); err == nil { + table.add("CPU time", cpuTime) + } + } table.print(w) } diff --git a/tsc/internal/osutil/cputime_other.go b/tsc/internal/osutil/cputime_other.go new file mode 100644 index 0000000000000..3b09c61fc3d67 --- /dev/null +++ b/tsc/internal/osutil/cputime_other.go @@ -0,0 +1,12 @@ +//go:build !unix && !windows + +package osutil + +import ( + "errors" + "time" +) + +func processCPUTime() (time.Duration, error) { + return 0, errors.ErrUnsupported +} diff --git a/tsc/internal/osutil/cputime_unix.go b/tsc/internal/osutil/cputime_unix.go new file mode 100644 index 0000000000000..ec9d3eb57bb16 --- /dev/null +++ b/tsc/internal/osutil/cputime_unix.go @@ -0,0 +1,17 @@ +//go:build unix + +package osutil + +import ( + "time" + + "golang.org/x/sys/unix" +) + +func processCPUTime() (time.Duration, error) { + var usage unix.Rusage + if err := unix.Getrusage(unix.RUSAGE_SELF, &usage); err != nil { + return 0, err + } + return time.Duration(usage.Utime.Nano()) + time.Duration(usage.Stime.Nano()), nil +} diff --git a/tsc/internal/osutil/cputime_windows.go b/tsc/internal/osutil/cputime_windows.go new file mode 100644 index 0000000000000..b25d84f897b8d --- /dev/null +++ b/tsc/internal/osutil/cputime_windows.go @@ -0,0 +1,17 @@ +package osutil + +import ( + "time" + + "golang.org/x/sys/windows" +) + +func processCPUTime() (time.Duration, error) { + var creation, exit, kernel, user windows.Filetime + if err := windows.GetProcessTimes(windows.CurrentProcess(), &creation, &exit, &kernel, &user); err != nil { + return 0, err + } + kernelTicks := uint64(kernel.HighDateTime)<<32 | uint64(kernel.LowDateTime) + userTicks := uint64(user.HighDateTime)<<32 | uint64(user.LowDateTime) + return time.Duration(kernelTicks+userTicks) * 100 * time.Nanosecond, nil +} diff --git a/tsc/internal/osutil/osutil.go b/tsc/internal/osutil/osutil.go index ed6bc22edd697..16d5d49972be9 100644 --- a/tsc/internal/osutil/osutil.go +++ b/tsc/internal/osutil/osutil.go @@ -1,5 +1,7 @@ package osutil +import "time" + // Args returns the command-line arguments with platform-specific launcher details removed. func Args() []string { return args() @@ -9,3 +11,9 @@ func Args() []string { func Executable() (string, error) { return executable() } + +// ProcessCPUTime returns cumulative user and system CPU time across all threads +// of the current process, excluding child processes. +func ProcessCPUTime() (time.Duration, error) { + return processCPUTime() +} From 4e0958badebc4693d8772602a6275f84ac74abe0 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:28:15 -0700 Subject: [PATCH 2/3] Include CPU time in ordinary diagnostics The performance runner requests ordinary diagnostics, not extended diagnostics, so it cannot collect CPU time when the metric requires the extended flag. --- tsc/internal/execute/build/orchestrator.go | 2 +- tsc/internal/execute/tsc/emit.go | 2 +- tsc/internal/execute/tsc/statistics.go | 8 +++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tsc/internal/execute/build/orchestrator.go b/tsc/internal/execute/build/orchestrator.go index 6a1db66d7fddd..664e47844616e 100644 --- a/tsc/internal/execute/build/orchestrator.go +++ b/tsc/internal/execute/build/orchestrator.go @@ -60,7 +60,7 @@ func (b *OrchestratorResult) reportWithFilesToDelete(o *Orchestrator, reportFile return } b.Statistics.SetTotalTime(o.opts.Sys.SinceStart()) - b.Statistics.Report(o.opts.Sys.Writer(), o.opts.Testing, o.opts.Command.CompilerOptions.ExtendedDiagnostics.IsTrue()) + b.Statistics.Report(o.opts.Sys.Writer(), o.opts.Testing) } type Orchestrator struct { diff --git a/tsc/internal/execute/tsc/emit.go b/tsc/internal/execute/tsc/emit.go index 44fa09179f810..4f30b1617ada1 100644 --- a/tsc/internal/execute/tsc/emit.go +++ b/tsc/internal/execute/tsc/emit.go @@ -60,7 +60,7 @@ func EmitAndReportStatistics(input EmitInput) (CompileAndEmitResult, *Statistics runtime.ReadMemStats(&memStats) statistics = statisticsFromProgram(input, &memStats) - statistics.Report(input.Writer, input.Testing, input.Config.CompilerOptions().ExtendedDiagnostics.IsTrue()) + statistics.Report(input.Writer, input.Testing) } if result.EmitResult.EmitSkipped && len(result.Diagnostics) > 0 { diff --git a/tsc/internal/execute/tsc/statistics.go b/tsc/internal/execute/tsc/statistics.go index f2b843ba88235..e49c2628d8b73 100644 --- a/tsc/internal/execute/tsc/statistics.go +++ b/tsc/internal/execute/tsc/statistics.go @@ -84,7 +84,7 @@ func statisticsFromProgram(input EmitInput, memStats *runtime.MemStats) *Statist } } -func (s *Statistics) Report(w io.Writer, testing CommandLineTesting, extendedDiagnostics bool) { +func (s *Statistics) Report(w io.Writer, testing CommandLineTesting) { if testing != nil { testing.OnStatisticsStart(w) defer testing.OnStatisticsEnd(w) @@ -127,10 +127,8 @@ func (s *Statistics) Report(w io.Writer, testing CommandLineTesting, extendedDia } s.addContentMapperStatistics(&table, prefix) table.add(prefix+"Total time", s.compileTimes.totalTime) - if extendedDiagnostics { - if cpuTime, err := osutil.ProcessCPUTime(); err == nil { - table.add("CPU time", cpuTime) - } + if cpuTime, err := osutil.ProcessCPUTime(); err == nil { + table.add("CPU time", cpuTime) } table.print(w) } From b78c4d2fc1aa0542b2be61ff594ca42388d78ebe Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:51:18 -0800 Subject: [PATCH 3/3] Keep total time last in compiler statistics CPU time and elapsed time describe different aspects of compiler work. Keep the elapsed total at the end as the overall compilation summary. --- tsc/internal/execute/tsc/statistics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsc/internal/execute/tsc/statistics.go b/tsc/internal/execute/tsc/statistics.go index e49c2628d8b73..0ad1a3bb08c8d 100644 --- a/tsc/internal/execute/tsc/statistics.go +++ b/tsc/internal/execute/tsc/statistics.go @@ -126,10 +126,10 @@ func (s *Statistics) Report(w io.Writer, testing CommandLineTesting) { table.add(prefix+"Changes compute time", s.compileTimes.ChangesComputeTime) } s.addContentMapperStatistics(&table, prefix) - table.add(prefix+"Total time", s.compileTimes.totalTime) if cpuTime, err := osutil.ProcessCPUTime(); err == nil { table.add("CPU time", cpuTime) } + table.add(prefix+"Total time", s.compileTimes.totalTime) table.print(w) }