- Methods: All Methods Documented Methods Hide Methods
- Source: Display Source Hide Source
- Variables: Show Variables Hide Variables
Class ::ns::stats::Sampler
::ns::stats::Samplercreate ...
Defined in
Class Relations
::nx::Class create ::ns::stats::Sampler \ -superclass ::ns::stats::CollectorMethods (to be applied on instances)
calculate (scripted, public)
<instance of ns::stats::Sampler> calculate current previous \ interval instance
Derive interval diagnostics from current and previous snapshots. Return the current observations augmented with counter deltas, rates, average request phase times and thread CPU percentages. Rates use measured elapsed time; interval records the configured sampling cadence, and instance identifies the sampler lifetime. Interval results require a previous snapshot from the same process lifetime and positive elapsed time. Missing baselines or decreased counters yield empty derived values. CPU comparisons match thread identifiers and creation times; percentages use one core as 100%. Does not collect observations or modify shared state.
- Parameters:
- current (required)
- Current snapshot dictionary, including its epoch-microsecond timestamp and process identity.
- previous (required)
- Previous snapshot dictionary, or empty when no baseline exists.
- interval (required)
- Configured sampling cadence in seconds; rates use actual elapsed time instead.
- instance (required)
- Sampler lifetime identifier attached to the calculated sample.
- Returns:
- Current observations augmented with sampling metadata, counter deltas, rates, average timings and CPU percentages; unavailable derived values remain empty.
- Testcases:
- No testcase defined.
set stamp [dict get $current timestamp] set elapsed {} set ready false set reset false if {$previous ne "" && [dict get $previous process pid] eq [dict get $current process pid] && [dict get $previous process bootTime] eq [dict get $current process bootTime] } { set elapsed [expr {($stamp-[dict get $previous timestamp])/1000000.0}] if {$elapsed > 0} { set ready true } else { set reset true } } set result $current dict set result instance $instance dict set result sampling [dict create ready $ready from {} to [expr {$stamp/1000000.0}] elapsedSeconds $elapsed ageSeconds 0 reset $reset intervalSeconds $interval] if {$ready} { dict set result sampling from [expr {[dict get $previous timestamp]/1000000.0}] } dict for {s server} [dict get $current servers] { dict for {p entry} [dict get $server pools] { set deltas {} set rates {} set invalid {} dict for {k v} [dict get $entry counters] { if {$k ni {requests queued spools spooled dropped queuetime filtertime runtime tracetime}} { continue } set d {} if {$ready && [dict exists $previous servers $s pools $p counters $k]} { set d [:delta $v [dict get $previous servers $s pools $p counters $k]] if {$d eq ""} { lappend invalid $k } } dict set deltas $k $d dict set rates $k [expr {$d eq "" ? "" : $d/$elapsed}] } dict set result servers $s pools $p deltas $deltas dict set result servers $s pools $p rates $rates dict set result servers $s pools $p resetCounters $invalid set timings {} set n [expr {[dict exists $deltas requests] ? [dict get $deltas requests] : ""}] foreach k {queuetime filtertime runtime tracetime} { set d [expr {[dict exists $deltas $k] ? [dict get $deltas $k] : ""}] dict set timings $k [expr {$n eq "" || $n == 0 || $d eq "" ? "" : $d/$n}] } dict set result servers $s pools $p timingAverageSeconds $timings } } dict for {thread entry} [dict get $current drivers] { set deltas {} set rates {} set invalid {} foreach k {received partial spooled errors} { set d {} if {$ready && [dict exists $entry counters $k] && [dict exists $previous drivers $thread counters $k]} { set d [:delta [dict get $entry counters $k] [dict get $previous drivers $thread counters $k]] if {$d eq ""} { lappend invalid $k } } dict set deltas $k $d dict set rates $k [expr {$d eq "" ? "" : $d/$elapsed}] } dict set result drivers $thread deltas $deltas dict set result drivers $thread rates $rates dict set result drivers $thread resetCounters $invalid } set percentages {} set totals {} set matched 0 dict for {id entry} [dict get $current cpu] { set percent {} if {$ready && [dict exists $previous cpu $id]} { set old [dict get $previous cpu $id] set user [:delta [dict get $entry user] [dict get $old user]] set system [:delta [dict get $entry system] [dict get $old system]] if {$user ne "" && $system ne ""} { set percent [expr {100.0*($user+$system)/($elapsed*1000000)}] incr matched } } set name [string trim [dict get $entry name] -] set group other foreach {pattern kind} {driver:* driver conn:* connection writer:* writer spooler:* spooler} { if {[string match $pattern $name]} { set group $kind break } } dict set percentages $id [dict create name $name group $group percent $percent] if {$percent ne ""} { set total [expr {[dict exists $totals $group] ? [dict get $totals $group] : 0.0}] dict set totals $group [expr {$total+$percent}] } } if {[dict exists $current process cpuCapacity throttling]} { dict for {path row} [dict get $current process cpuCapacity throttling] { foreach field {periods throttledPeriods throttledSeconds} { set delta {} if {$ready && [dict exists $row $field] && [dict exists $previous process cpuCapacity throttling $path $field]} { set old [dict get $previous process cpuCapacity throttling $path $field] if {[dict get $row $field]>=$old} {set delta [expr {[dict get $row $field]-$old}]} } dict set row delta[string toupper [string index $field 0]][string range $field 1 end] $delta } dict set row throttledPeriodPercent {} if {[dict get $row deltaPeriods] ne "" && [dict get $row deltaPeriods]>0 && [dict get $row deltaThrottledPeriods] ne ""} { dict set row throttledPeriodPercent [expr {100.0*[dict get $row deltaThrottledPeriods]/[dict get $row deltaPeriods]}] } dict set result process cpuCapacity throttling $path $row } } dict set result cpuPercent [dict create scope process matchedThreads $matched observedThreads [dict size [dict get $current cpu]] groups $totals threads $percentages] return $resultsample (scripted, public)
<instance of ns::stats::Sampler> sample [ -server server ] \ [ -window window ]
Return the latest published sample, updating its reported age and running state. Before any publication, return a dictionary with sampling readiness false and no interval observations. An optional server argument restricts virtual-server data; CPU measurements remain process-wide. Without -window, does not collect observations or advance the baseline. With -window (0..900 seconds, time units accepted), collect a fresh snapshot and compare it with the nearest retained endpoint to that age (within half a sampling interval). Missing history, gaps or sampler restarts leave readiness false. The measured elapsedSeconds reports the actual selected period. The internal raw baseline is omitted from the returned dictionary.
- Switches:
- -server (optional)
- Optional virtual-server name; empty returns all servers. CPU metrics remain process-wide.
- -window (optional, defaults to
"0")- Requested display period, accepting ns_baseunit time syntax; 0..900 seconds, default 0 for the latest published sample.
- Returns:
- Sample dictionary with sampling readiness and age; a positive window adds freshly calculated interval metrics when a suitable continuous baseline exists.
- Testcases:
- No testcase defined.
if {$server ne "" && $server ni [ns_info servers]} { error "unknown server: $server" } set window [ns_baseunit -time $window] if {$window < 0 || $window > 900} { error "display window must be 0..900 seconds" } :locked { set result [nsv_get ns_stats pair] set recent [expr {$window > 0 ? [nsv_get ns_stats recent] : {}}] } if {$result eq ""} { return [dict create schemaVersion 1 sampling {ready false from {} to {} elapsedSeconds {} ageSeconds {} reset false} servers {} availability {}] } if {$window > 0} { set current [:snapshot] set target [expr {[dict get $current timestamp]-$window*1000000}] set baseline {} set distance Inf foreach record $recent { if {[dict get $record timestamp] >= [dict get $current timestamp] || [dict get $record instance] ne [dict get $result instance]} { continue } set difference [expr {abs([dict get $record timestamp]-$target)}] if {$difference < $distance} { set baseline $record set distance $difference } } set cadence [dict get $result sampling intervalSeconds] set reason {} if {$baseline eq "" || $distance > 0.5*$cadence*1000000} { set baseline {} set reason insufficient-history } else { set previous $baseline foreach record $recent { if {[dict get $record timestamp] <= [dict get $baseline timestamp] || [dict get $record timestamp] > [dict get $current timestamp]} { continue } if {[dict get $record instance] ne [dict get $result instance] || ![dict get $record sampling ready] || [dict get $record timestamp]-[dict get $previous timestamp] > 1.5*max($cadence,[dict get $previous sampling intervalSeconds])*1000000} { set reason sampling-gap set baseline {} break } set previous $record } } if {$baseline ne "" && [dict get $current timestamp]-[dict get $previous timestamp] > 1.5*$cadence*1000000} { set baseline {} set reason sampling-gap } # Older retained records may predate CPU baseline retention. if {$baseline ne "" && ![dict exists $baseline cpu]} { dict set baseline cpu {} } set result [:calculate $current $baseline $cadence [dict get $result instance]] dict set result sampling requestedSeconds $window dict set result sampling incompleteReason $reason } dict set result sampling running [dict get [nsv_get ns_stats control] running] dict set result sampling ageSeconds [expr {max(0,([clock microseconds]-[dict get $result timestamp])/1000000.0)}] if {[dict exists $result raw]} {dict unset result raw} # CPU is process-wide; filtering server pools never attributes CPU to a vhost. return [:selectServer $result $server]sampling history (scripted, public)
<instance of ns::stats::Sampler> sampling history -from from \ -to to [ -server server ] [ -limit limit ] [ -cursor cursor ]
Read the bounded process-wide recent sample buffer without SQLite. Retains at most 180 compact samples and 15 minutes at publication. Returns interval metrics, coverage and timestamp-based paging. Raw/per-thread CPU counters are omitted. Reading does not collect, start sampling, advance baselines or write persistent history. The buffer survives sampler stop/start but not a process restart.
- Switches:
- -from (required, integer)
- Inclusive lower bound in epoch seconds; must be nonnegative.
- -to (required, integer)
- Exclusive upper bound in epoch seconds; after from, at most 24 hours later and no later than now.
- -server (optional)
- Optional historical virtual-server name; empty returns all stored server data.
- -limit (optional, integer, defaults to
"180")- Maximum records per page, 1..180; default 180.
- -cursor (optional, integer, defaults to
"0")- Nonnegative timestamp cursor in microseconds, default 0; pass nextCursor unchanged for continuation.
- Returns:
- Dictionary containing records, retained bounds, coverage, more and nextCursor for the in-memory history buffer.
- Testcases:
- No testcase defined.
if {$from<0 || $to<=$from || $to-$from>86400 || $to>[clock seconds] || $limit<1 || $limit>180 || $cursor<0} { error "expected an epoch window up to 24 hours, limit 1..180 and nonnegative cursor" } if {[string length $server]>512 || [string first \x00 $server]>=0} { error "invalid server name" } :locked { set retained [nsv_get ns_stats recent] set dropped [nsv_get ns_stats recentDroppedThrough] } set records {} set gaps {} set restarts {} set resets 0 set previous {} set first {} set last {} set more false foreach record $retained { set stamp [dict get $record timestamp] if {$first eq ""} { set first $stamp } set last $stamp if {$stamp<$from*1000000 || $stamp<=$cursor} { set previous $record continue } if {$stamp>=$to*1000000} { # A gap crossing the upper bound must not certify coverage. if {$previous ne ""} { set delta [expr {($stamp-[dict get $previous timestamp])/1000000.0}] if {$delta>1.5*max([dict get $previous sampling intervalSeconds], [dict get $record sampling intervalSeconds])} { lappend gaps [dict create from [dict get $previous sampling to] to [dict get $record sampling to]] } } break } if {[llength $records]>=$limit} { set more true break } if {![dict get $record sampling ready]} { incr resets } if {$previous ne ""} { if {[dict get $previous instance] ne [dict get $record instance]} { lappend restarts [dict get $record sampling to] } set delta [expr {($stamp-[dict get $previous timestamp])/1000000.0}] if {$delta>1.5*max([dict get $previous sampling intervalSeconds], [dict get $record sampling intervalSeconds])} { lappend gaps [dict create from [dict get $previous sampling to] to [dict get $record sampling to]] } } lappend records [:selectServer [dict remove $record cpu] $server false] set previous $record } # Retained bounds refer to the buffer, independently of the selected page. if {$retained ne ""} { set first [dict get [lindex $retained 0] timestamp] set last [dict get [lindex $retained end] timestamp] } set overlap [expr {$dropped ne "" && $dropped>=$from*1000000}] if {!$more && $cursor>0} { set window [:sampling history -from $from -to $to -server $server -limit 180] set coverage [dict get $window coverage] return [dict create schemaVersion 1 source memory from $from to $to records $records nextCursor {} more false retentionSeconds 900 maxSamples 180 coverage $coverage] } set complete [expr {$first ne "" && $first<=$from*1000000 && $last>=$to*1000000 && !$more && $cursor==0 && !$overlap && $gaps eq "" && $restarts eq "" && $resets==0}] set next {} if {$more} { set next [dict get [lindex $records end] timestamp] } return [dict create schemaVersion 1 source memory from $from to $to records $records nextCursor $next more $more retentionSeconds 900 maxSamples 180 coverage [dict create complete $complete windowExamined [expr {!$more && $cursor==0}] retainedFrom [expr {$first eq "" ? 0 : $first/1000000.0}] retainedTo [expr {$last eq "" ? 0 : $last/1000000.0}] gaps $gaps restartBoundaries $restarts resetSamples $resets evictionOrFailureOverlap $overlap droppedThrough $dropped]]sampling start (scripted, public)
<instance of ns::stats::Sampler> sampling start \ [ -interval interval ]
Start the process-wide sampling callback at the requested interval in seconds (default 10, range 1..3600). Time units and fractional seconds are normalized with ns_baseunit. Repeated calls with the same running interval reuse the scheduler; a different interval requires stopping the sampler first. Clears the previous sample and baseline when starting a stopped sampler. Returns the resulting status dictionary. Persistence must be enabled separately.
- Switches:
- -interval (optional, defaults to
"10")- Sampling cadence, accepting ns_baseunit time syntax; 1..3600 seconds, default 10 seconds.
- Returns:
- Resulting sampling status dictionary.
- Testcases:
- No testcase defined.
set interval [ns_baseunit -time $interval] if {$interval < 1 || $interval > 3600} { error "interval must be 1..3600 seconds" } :locked { set state [nsv_get ns_stats control] if {[dict get $state running]} { if {[dict get $state interval]!=$interval} { error "sampler already running with a different interval; stop it before reconfiguring" } } else { dict incr state generation dict set state running true dict set state interval $interval dict set state busy false dict set state scheduler [ns_schedule_proc -thread $interval [self] sampling tick] nsv_set ns_stats pair {} nsv_set ns_stats control $state } } return [:sampling status]sampling status (scripted, public)
<instance of ns::stats::Sampler> sampling status
Return the process-wide sampler configuration and status, including running state, scheduler identifier, interval, process instance, collection errors and readiness of the latest interval sample. latestAt is in epoch seconds; ageSeconds is the elapsed time since the latest publication. Both are empty before the first sample. Does not collect new observations or modify sampler state.
- Returns:
- Status dictionary including running, interval in seconds, scheduler, instance, errors, lastError, ready, latestAt and ageSeconds.
- Testcases:
- No testcase defined.
set result [nsv_get ns_stats control] dict unset result busy dict unset result generation set sample [nsv_get ns_stats pair] dict set result ready [expr {$sample ne "" && [dict get $sample sampling ready]}] dict set result latestAt {} dict set result ageSeconds {} if {$sample ne ""} { set stamp [dict get $sample timestamp] dict set result latestAt [expr {$stamp/1000000.0}] dict set result ageSeconds [expr {max(0,([clock microseconds]-$stamp)/1000000.0)}] } return $resultsampling stop (scripted, public)
<instance of ns::stats::Sampler> sampling stop
Unschedule collection and invalidate any collection already in progress so it cannot publish into the stopped sampler. Retains the latest published sample for subsequent reads. Returns the resulting status dictionary. Does not disable the separately configured history persistence callback.
- Returns:
- Resulting sampling status dictionary; the last published sample remains available.
- Testcases:
- No testcase defined.
:locked { set state [nsv_get ns_stats control] if {[dict get $state running]} { ns_unschedule_proc [dict get $state scheduler] } dict set state running false dict incr state generation dict set state busy false dict set state scheduler {} nsv_set ns_stats control $state } return [:sampling status]sampling tick (scripted, public)
<instance of ns::stats::Sampler> sampling tick
Perform one scheduled collection and publish its interval sample. Skip collection when the sampler is stopped or another collection is in progress. Collect observations outside the coordination lock, then calculate and publish only if the sampler generation is still current. Retain the raw snapshot as the next interval baseline. Collection failures retain the previous publication, update error status and emit a warning. Does not write SQLite history; persistence runs through a separate callback.
- Testcases:
- No testcase defined.
set generation {} :locked { set state [nsv_get ns_stats control] if {![dict get $state running] || [dict get $state busy]} { return } set generation [dict get $state generation] dict set state busy true nsv_set ns_stats control $state } if {$generation eq ""} { return } try { set current [:snapshot] :locked { set state [nsv_get ns_stats control] if {$generation!=[dict get $state generation]} { return } set pair [nsv_get ns_stats pair] set previous [expr {$pair eq "" ? "" : [dict get $pair raw]}] set sample [:calculate $current $previous [dict get $state interval] [dict get $state instance]] :retainSample $sample dict set sample raw $current nsv_set ns_stats pair $sample dict set state busy false dict set state lastError {} nsv_set ns_stats control $state } } on error {message options} { :locked { set state [nsv_get ns_stats control] if {$generation == [dict get $state generation]} { dict set state busy false dict incr state errors dict set state lastError $message nsv_set ns_stats control $state } } ns_log warning "ns_stats: sampling failed: $message" }
- Methods: All Methods Documented Methods Hide Methods
- Source: Display Source Hide Source
- Variables: Show Variables Hide Variables
![[i]](/resources/acs-subsite/ZoomIn16.gif)