mirror of
https://github.com/logos-storage/nim-chroprof.git
synced 2026-01-04 06:23:11 +00:00
track children metrics correctly even when they finish after the parent
This commit is contained in:
parent
127378e32f
commit
44af970bc9
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,3 +2,5 @@ nimble.develop
|
|||||||
nimble.paths
|
nimble.paths
|
||||||
|
|
||||||
build
|
build
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
|||||||
1
chroprof/config.nim
Normal file
1
chroprof/config.nim
Normal file
@ -0,0 +1 @@
|
|||||||
|
const chroprofDebug* {.booldefine.}: bool = false
|
||||||
@ -23,6 +23,11 @@ type
|
|||||||
|
|
||||||
EventCallback* = proc(e: Event) {.nimcall, gcsafe, raises: [].}
|
EventCallback* = proc(e: Event) {.nimcall, gcsafe, raises: [].}
|
||||||
|
|
||||||
|
const FinishStates* = [
|
||||||
|
ExtendedFutureState.Completed, ExtendedFutureState.Cancelled,
|
||||||
|
ExtendedFutureState.Failed,
|
||||||
|
]
|
||||||
|
|
||||||
var handleFutureEvent {.threadvar.}: EventCallback
|
var handleFutureEvent {.threadvar.}: EventCallback
|
||||||
|
|
||||||
proc `location`*(self: Event): SrcLoc =
|
proc `location`*(self: Event): SrcLoc =
|
||||||
|
|||||||
@ -5,7 +5,9 @@
|
|||||||
import std/[tables, options, sets]
|
import std/[tables, options, sets]
|
||||||
import chronos/[timer, srcloc]
|
import chronos/[timer, srcloc]
|
||||||
|
|
||||||
|
import ./config
|
||||||
import ./events
|
import ./events
|
||||||
|
import ./utils
|
||||||
|
|
||||||
export timer, tables, sets, srcloc
|
export timer, tables, sets, srcloc
|
||||||
|
|
||||||
@ -48,16 +50,23 @@ type
|
|||||||
## for this `FutureType`.
|
## for this `FutureType`.
|
||||||
|
|
||||||
PartialMetrics = object
|
PartialMetrics = object
|
||||||
state*: ExtendedFutureState
|
## Tracks `PartialMetric`s for a single run of a given `Future`. `PartialMetrics`
|
||||||
|
## may not be complete until the `Future` and its children have reached a
|
||||||
|
## finish state.
|
||||||
created*: Moment
|
created*: Moment
|
||||||
lastStarted*: Moment
|
lastStarted*: Moment
|
||||||
timeToFirstPause*: Duration
|
timeToFirstPause*: Duration
|
||||||
partialExecTime*: Duration
|
partialExecTime*: Duration
|
||||||
partialChildrenExecTime*: Duration
|
partialChildrenExecTime*: Duration
|
||||||
partialChildrenExecOverlap*: Duration
|
partialChildrenExecOverlap*: Duration
|
||||||
parent*: Option[uint]
|
wallclockTime: Duration
|
||||||
pauses*: uint
|
pauses*: uint
|
||||||
|
|
||||||
|
futureType: FutureType
|
||||||
|
state*: ExtendedFutureState
|
||||||
|
parent*: Option[uint]
|
||||||
|
liveChildren: uint
|
||||||
|
|
||||||
MetricsTotals* = Table[FutureType, AggregateMetrics]
|
MetricsTotals* = Table[FutureType, AggregateMetrics]
|
||||||
|
|
||||||
ProfilerState* = object
|
ProfilerState* = object
|
||||||
@ -68,36 +77,32 @@ type
|
|||||||
proc `execTimeWithChildren`*(self: AggregateMetrics): Duration =
|
proc `execTimeWithChildren`*(self: AggregateMetrics): Duration =
|
||||||
self.execTime + self.childrenExecTime
|
self.execTime + self.childrenExecTime
|
||||||
|
|
||||||
proc push(self: var seq[uint], value: uint): void =
|
|
||||||
self.add(value)
|
|
||||||
|
|
||||||
proc pop(self: var seq[uint]): uint =
|
|
||||||
let value = self[^1]
|
|
||||||
self.setLen(self.len - 1)
|
|
||||||
value
|
|
||||||
|
|
||||||
proc peek(self: var seq[uint]): Option[uint] =
|
|
||||||
if self.len == 0:
|
|
||||||
none(uint)
|
|
||||||
else:
|
|
||||||
self[^1].some
|
|
||||||
|
|
||||||
proc `$`(location: SrcLoc): string =
|
|
||||||
$location.procedure & "[" & $location.file & ":" & $location.line & "]"
|
|
||||||
|
|
||||||
proc futureCreated(self: var ProfilerState, event: Event): void =
|
proc futureCreated(self: var ProfilerState, event: Event): void =
|
||||||
assert not self.partials.hasKey(event.futureId), $event.location
|
assert not self.partials.hasKey(event.futureId), $event.location
|
||||||
|
|
||||||
self.partials[event.futureId] =
|
self.partials[event.futureId] =
|
||||||
PartialMetrics(created: event.timestamp, state: Pending)
|
PartialMetrics(created: event.timestamp, state: Pending, futureType: event.location)
|
||||||
|
|
||||||
proc bindParent(self: var ProfilerState, metrics: ptr PartialMetrics): void =
|
proc bindParent(self: var ProfilerState, metrics: ptr PartialMetrics): void =
|
||||||
let current = self.callStack.peek()
|
let current = self.callStack.peek()
|
||||||
if current.isNone:
|
if current.isNone:
|
||||||
|
when chroprofDebug:
|
||||||
|
echo "No parent for ", $metrics.futureType.procedure, ", ", $metrics.state
|
||||||
return
|
return
|
||||||
|
|
||||||
if metrics.parent.isSome:
|
if metrics.parent.isSome:
|
||||||
assert metrics.parent.get == current.get
|
assert metrics.parent.get == current.get
|
||||||
|
|
||||||
|
self.partials.withValue(current.get, parentMetrics):
|
||||||
|
parentMetrics.liveChildren += 1
|
||||||
|
when chroprofDebug:
|
||||||
|
echo "SET_PARENT: Parent of ",
|
||||||
|
$metrics.futureType.procedure,
|
||||||
|
" is ",
|
||||||
|
$parentMetrics.futureType.procedure,
|
||||||
|
", ",
|
||||||
|
$metrics.state
|
||||||
|
|
||||||
metrics.parent = current
|
metrics.parent = current
|
||||||
|
|
||||||
proc futureRunning(self: var ProfilerState, event: Event): void =
|
proc futureRunning(self: var ProfilerState, event: Event): void =
|
||||||
@ -128,40 +133,88 @@ proc futurePaused(self: var ProfilerState, event: Event): void =
|
|||||||
metrics.pauses += 1
|
metrics.pauses += 1
|
||||||
metrics.state = Paused
|
metrics.state = Paused
|
||||||
|
|
||||||
proc futureCompleted(self: var ProfilerState, event: Event): void =
|
proc aggregatePartial(
|
||||||
let location = event.location
|
self: var ProfilerState, metrics: ptr PartialMetrics, futureId: uint
|
||||||
if not self.metrics.hasKey(location):
|
): void =
|
||||||
self.metrics[location] = AggregateMetrics()
|
## Aggregates partial execution metrics into the total metrics for the given
|
||||||
|
## `FutureType`.
|
||||||
|
|
||||||
self.metrics.withValue(location, aggMetrics):
|
self.metrics.withValue(metrics.futureType, aggMetrics):
|
||||||
if not self.partials.hasKey(event.futureId):
|
let execTime = metrics.partialExecTime - metrics.partialChildrenExecOverlap
|
||||||
# Stillborn futures are those born in a finish state. We count those cause
|
|
||||||
# they may also be a byproduct of a bug.
|
aggMetrics.callCount.inc()
|
||||||
aggMetrics.stillbornCount.inc()
|
aggMetrics.execTime += execTime
|
||||||
|
aggMetrics.execTimeMax = max(aggMetrics.execTimeMax, execTime)
|
||||||
|
aggMetrics.childrenExecTime += metrics.partialChildrenExecTime
|
||||||
|
aggMetrics.wallClockTime += metrics.wallclockTime
|
||||||
|
|
||||||
|
if metrics.parent.isSome:
|
||||||
|
self.partials.withValue(metrics.parent.get, parentMetrics):
|
||||||
|
when chroprofDebug:
|
||||||
|
echo $metrics.futureType.procedure,
|
||||||
|
": add <<",
|
||||||
|
metrics.timeToFirstPause,
|
||||||
|
">> overlap and <<",
|
||||||
|
metrics.partialExecTime,
|
||||||
|
">> child exec time to parent (",
|
||||||
|
parentMetrics.futureType.procedure,
|
||||||
|
")"
|
||||||
|
|
||||||
|
parentMetrics.partialChildrenExecTime += metrics.partialExecTime
|
||||||
|
parentMetrics.partialChildrenExecOverlap += metrics.timeToFirstPause
|
||||||
|
parentMetrics.liveChildren -= 1
|
||||||
|
|
||||||
|
if parentMetrics.state in FinishStates:
|
||||||
|
if parentMetrics.liveChildren == 0:
|
||||||
|
when chroprofDebug:
|
||||||
|
echo "Perfom deferred aggregation of completed parent with no live children: ",
|
||||||
|
$parentMetrics.futureType.procedure
|
||||||
|
self.aggregatePartial(parentMetrics, metrics.parent.get)
|
||||||
|
else:
|
||||||
|
when chroprofDebug:
|
||||||
|
echo "Parent ",
|
||||||
|
$parentMetrics.futureType.procedure,
|
||||||
|
" still has ",
|
||||||
|
parentMetrics.liveChildren,
|
||||||
|
" live children"
|
||||||
|
|
||||||
|
self.partials.del(futureId)
|
||||||
|
|
||||||
|
proc countStillborn(self: var ProfilerState, futureType: FutureType): void =
|
||||||
|
self.metrics.withValue(futureType, aggMetrics):
|
||||||
|
aggMetrics.stillbornCount.inc()
|
||||||
|
|
||||||
|
proc futureCompleted(self: var ProfilerState, event: Event): void =
|
||||||
|
let futureType = event.location
|
||||||
|
let futureId = event.futureId
|
||||||
|
|
||||||
|
if not self.metrics.hasKey(futureType):
|
||||||
|
self.metrics[futureType] = AggregateMetrics()
|
||||||
|
|
||||||
|
if not self.partials.hasKey(futureId):
|
||||||
|
self.countStillborn(futureType)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.partials.withValue(futureId, partial):
|
||||||
|
if partial.state == Running:
|
||||||
|
self.futurePaused(event)
|
||||||
|
partial.state = event.newState
|
||||||
|
|
||||||
|
partial.wallclockTime = event.timestamp - partial.created
|
||||||
|
|
||||||
|
# Future still have live children, don't aggregate yet.
|
||||||
|
if partial.liveChildren > 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.partials.withValue(event.futureId, metrics):
|
self.aggregatePartial(partial, futureId)
|
||||||
if metrics.state == Running:
|
|
||||||
self.futurePaused(event)
|
|
||||||
|
|
||||||
let execTime = metrics.partialExecTime - metrics.partialChildrenExecOverlap
|
|
||||||
|
|
||||||
aggMetrics.callCount.inc()
|
|
||||||
aggMetrics.execTime += execTime
|
|
||||||
aggMetrics.execTimeMax = max(aggMetrics.execTimeMax, execTime)
|
|
||||||
aggMetrics.childrenExecTime += metrics.partialChildrenExecTime
|
|
||||||
aggMetrics.wallClockTime += event.timestamp - metrics.created
|
|
||||||
|
|
||||||
if metrics.parent.isSome:
|
|
||||||
self.partials.withValue(metrics.parent.get, parentMetrics):
|
|
||||||
parentMetrics.partialChildrenExecTime += metrics.partialExecTime
|
|
||||||
parentMetrics.partialChildrenExecOverlap += metrics.timeToFirstPause
|
|
||||||
|
|
||||||
self.partials.del(event.futureId)
|
|
||||||
|
|
||||||
proc processEvent*(
|
proc processEvent*(
|
||||||
self: var ProfilerState, event: Event
|
self: var ProfilerState, event: Event
|
||||||
): void {.nimcall, gcsafe, raises: [].} =
|
): void {.nimcall, gcsafe, raises: [].} =
|
||||||
|
when chroprofDebug:
|
||||||
|
echo "EVENT:",
|
||||||
|
$event.location.procedure, ", ", event.newState, ", ", event.timestamp
|
||||||
|
|
||||||
case event.newState
|
case event.newState
|
||||||
of Pending:
|
of Pending:
|
||||||
self.futureCreated(event)
|
self.futureCreated(event)
|
||||||
|
|||||||
19
chroprof/utils.nim
Normal file
19
chroprof/utils.nim
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import std/options
|
||||||
|
import chronos/srcloc
|
||||||
|
|
||||||
|
proc push*(self: var seq[uint], value: uint): void =
|
||||||
|
self.add(value)
|
||||||
|
|
||||||
|
proc pop*(self: var seq[uint]): uint =
|
||||||
|
let value = self[^1]
|
||||||
|
self.setLen(self.len - 1)
|
||||||
|
value
|
||||||
|
|
||||||
|
proc peek*(self: var seq[uint]): Option[uint] =
|
||||||
|
if self.len == 0:
|
||||||
|
none(uint)
|
||||||
|
else:
|
||||||
|
self[^1].some
|
||||||
|
|
||||||
|
proc `$`*(location: SrcLoc): string =
|
||||||
|
$location.procedure & "[" & $location.file & ":" & $location.line & "]"
|
||||||
@ -274,3 +274,31 @@ suite "profiler metrics":
|
|||||||
).sum
|
).sum
|
||||||
|
|
||||||
check stillborns == 2
|
check stillborns == 2
|
||||||
|
|
||||||
|
test "should properly account for children's execution time even when those finish after the parent":
|
||||||
|
let barrier = newFuture[void]("barrier")
|
||||||
|
|
||||||
|
proc child() {.async.} =
|
||||||
|
await barrier
|
||||||
|
advanceTime(10.milliseconds)
|
||||||
|
|
||||||
|
proc parent(): Future[Future[void]] {.async.} =
|
||||||
|
advanceTime(10.milliseconds)
|
||||||
|
return child()
|
||||||
|
|
||||||
|
let childFut = waitFor parent()
|
||||||
|
barrier.complete()
|
||||||
|
waitFor childFut
|
||||||
|
|
||||||
|
var metrics = recordedMetrics()
|
||||||
|
let parentMetrics = metrics.forProc("parent")
|
||||||
|
let childMetrics = metrics.forProc("child")
|
||||||
|
|
||||||
|
check parentMetrics.execTime == 10.milliseconds
|
||||||
|
check parentMetrics.childrenExecTime == 10.milliseconds
|
||||||
|
check parentMetrics.wallClockTime == 10.milliseconds
|
||||||
|
|
||||||
|
echo "EXE TIME ", $childMetrics.execTime
|
||||||
|
check childMetrics.execTime == 10.milliseconds
|
||||||
|
check childMetrics.wallClockTime == 10.milliseconds
|
||||||
|
check childMetrics.childrenExecTime == ZeroDuration
|
||||||
|
|||||||
@ -10,7 +10,7 @@ type SimpleEvent* = object
|
|||||||
# global vars means we can't really do much better for now.
|
# global vars means we can't really do much better for now.
|
||||||
var recording*: seq[SimpleEvent]
|
var recording*: seq[SimpleEvent]
|
||||||
var rawRecording*: seq[Event]
|
var rawRecording*: seq[Event]
|
||||||
var fakeTime*: Moment = Moment.now()
|
var fakeTime*: Moment = Moment.init(0, 1.milliseconds)
|
||||||
|
|
||||||
proc recordEvent(event: Event) {.nimcall, gcsafe, raises: [].} =
|
proc recordEvent(event: Event) {.nimcall, gcsafe, raises: [].} =
|
||||||
{.cast(gcsafe).}:
|
{.cast(gcsafe).}:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user