mirror of
https://github.com/nim-lang/Nim.git
synced 2026-09-03 12:20:27 +00:00
The frontend's loading is 3.55s of Atlas's 9.2s and had no breakdown
finer than "TopLevel 1459ms". Now it does — `tTopReplay`, `tTopLogOps`,
`tTopOffers`, `tTopStmts` and a counter for the records the loader
skips:
TopLevel 1459ms = Offers 569 + ExportBranch 312 + LogOps 137
+ the bare cursor walk ~371 + Replay/Stmts ~11
That immediately suggests a target, and it is a trap. 80% of every
module header the loader walks is tooling-only records — `sig`, one per
signature-symbol occurrence, plus `expansion`/`modulesrc` — which it
skips on sight and which exist only for `idetools`: 3.36M of 4.19M nodes
on Atlas. Grouping them under one tag, or moving them past the
`(implementation)` marker where the loop stops, is an easy change and
buys nothing.
Probed before building any of it, by emitting none of them at all:
nodes walked 4.19M -> 0.85M .s.bif 44.7MB -> 44.0MB
TopLevel 1459ms -> 1406ms frontend 9.29s -> 9.18s
Walking 3.3M records costs 53ms. `skip` on a `TagLit` is a jump, not a
scan — about 16ns a node — so the count was never the cost. A format
change for 0.5% would have been a bad trade discovered late.
What is left is the per-process re-load itself: 180 `nim m` each parsing
~20 modules' interfaces out of 44.7MB of `.s.bif`, with no dominant item
because there is no single item. It is the amortisation problem batching
already solved for the backend (`loadDepClosure` 10.2s -> 1.3s), and the
frontend is where it has not been solved.
Behind `-d:icBNodeProf`. ic 42/42, `koch boot -d:release` equal
executables.
115 lines
4.8 KiB
Nim
115 lines
4.8 KiB
Nim
#
|
|
#
|
|
# The Nim Compiler
|
|
# (c) Copyright 2026 Andreas Rumpf
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
#
|
|
|
|
## Opt-in instrumentation for the IC backend, enabled with `-d:icBNodeProf`.
|
|
## Off, every template below is `discard` and nothing is linked in.
|
|
##
|
|
## It lives in its own module with NO compiler imports so that any stage can
|
|
## use it without creating a cycle — `bnode` needs it for the accessors,
|
|
## `nifbackend` for the stage phases, `cgen` for what happens per routine.
|
|
##
|
|
## Each backend process appends ONE line to `$NIM_IC_BNODE_PROF` at exit (or to
|
|
## stderr when that is unset), because a `--ic:on` build fans out a process per
|
|
## module per stage and interleaved writes would tear. Use `-d:icNoParallel`
|
|
## when the numbers need to be attributable to a particular module.
|
|
##
|
|
## Counts are for volume, timings for cost, and the two answer different
|
|
## questions: the accessors turned out to be 700k calls worth 8ms, while `info`
|
|
## was 259k calls worth 1.36s. Neither number alone would have found that.
|
|
|
|
when defined(icBNodeProf):
|
|
import std / [envvars, exitprocs, syncio, monotimes]
|
|
from std / times import inNanoseconds
|
|
|
|
type
|
|
ProfSlot* = enum
|
|
pKind, pTagKindHit, pTagKindMiss, pAstChildren, pSkip, pSon, pLen,
|
|
pLastSon, pIterYield, pSym, pTyp, pTypTagLit, pOrigin, pNilType,
|
|
pGenBodyCalls, pInfo, pIfaceExported, pIfaceHidden, pIfaceModules,
|
|
pTopNodes, pExportSyms, pPeekKind, pPeekFallback, pPeekLoaded,
|
|
pTopToolingSkip
|
|
TimeSlot* = enum
|
|
tLoadClosure, tModuleId, tBifLoad, tPosIndex, tTopLevel, tInterfTables,
|
|
tTransform, tHandOff, tGenBody, tAnalyses,
|
|
tSym, tTyp, tInfo, tOrigin, tExportBranch, tResolveSym, tEnumFields,
|
|
# Coarse phases, added to find where a backend process spends the time
|
|
# that none of the slots above account for. `tStage` is the whole stage
|
|
# body, so `Process - tStage` is everything before it: exec, the Nim
|
|
# runtime, config replay, `registerNifSuffix`/graph setup.
|
|
tStage,
|
|
tLowerOwned, tLowerHooks, tLowerWrite,
|
|
tCgGen, tCgInit, tCgFinish, tCgWrite,
|
|
tMergeStage, tEmitRender, tLinkStage,
|
|
# `nim m` (the frontend): the sem pass as a whole, and writing the module's
|
|
# `.s.bif`. `Stage - WriteNif - <the loading slots>` is then sem proper.
|
|
tWriteNif,
|
|
# `processTopLevel`'s branches: which part of a module HEADER costs what.
|
|
tTopReplay, tTopLogOps, tTopOffers, tTopStmts
|
|
|
|
let procStart = getMonoTime()
|
|
## Set when this module initialises, i.e. essentially at process start, so
|
|
## the dump can report total process wall time and the startup share can be
|
|
## derived as `Process - Stage`.
|
|
|
|
var profStageName* = "frontend"
|
|
## Which invocation this is: the backend stage name, or "frontend" for a
|
|
## `nim m` process, which arms the profiler through ast2nif but never enters
|
|
## a backend stage. Without it the `Process - Stage` startup figure is
|
|
## meaningless — 204 frontend processes' whole runtime lands in it.
|
|
|
|
var profCounts: array[ProfSlot, int]
|
|
var profNanos: array[TimeSlot, int64]
|
|
var profStart: array[TimeSlot, MonoTime]
|
|
var profArmed = false
|
|
|
|
proc profDump() =
|
|
var line = "BNODEPROF stage=" & profStageName
|
|
for s in ProfSlot: line.add " " & ($s)[1..^1] & "=" & $profCounts[s]
|
|
for s in TimeSlot: line.add " " & ($s)[1..^1] & "ms=" & $(profNanos[s] div 1_000_000)
|
|
line.add " Processms=" & $((getMonoTime() - procStart).inNanoseconds div 1_000_000)
|
|
let f = getEnv("NIM_IC_BNODE_PROF")
|
|
if f.len > 0:
|
|
let h = open(f, fmAppend)
|
|
h.writeLine line
|
|
h.close()
|
|
else:
|
|
stderr.writeLine line
|
|
|
|
template armProf() =
|
|
if not profArmed:
|
|
profArmed = true
|
|
addExitProc profDump
|
|
|
|
template prof*(s: ProfSlot; n = 1) =
|
|
armProf()
|
|
inc profCounts[s], n
|
|
template icProfStart*(s: TimeSlot) =
|
|
armProf()
|
|
profStart[s] = getMonoTime()
|
|
template icProfStop*(s: TimeSlot) =
|
|
profNanos[s] += (getMonoTime() - profStart[s]).inNanoseconds
|
|
|
|
template timed*(s: TimeSlot; body: untyped) =
|
|
## Leaf timing. NOT re-entrant, and the phase slots are not disjoint —
|
|
## `tTransform` contains body materialization, `tTyp` reaches `tSym`. Read
|
|
## them as nested, not additive.
|
|
##
|
|
## Arms the dump like `prof`/`icProfStart` do. It did not, and so a process
|
|
## whose ONLY instrumentation is a `timed` never reported at all: the
|
|
## `merge`, `emit` and `link` stages were silently absent from every profile.
|
|
armProf()
|
|
let t0 = getMonoTime()
|
|
body
|
|
profNanos[s] += (getMonoTime() - t0).inNanoseconds
|
|
else:
|
|
template prof*(s: untyped; n = 1) = discard
|
|
template icProfStart*(s: untyped) = discard
|
|
template icProfStop*(s: untyped) = discard
|
|
template timed*(s: untyped; body: untyped) = body
|