Class ::ns::stats::Collector (public)

 ::nx::Class ::ns::stats::Collector[i]

Defined in

Testcases:
No testcase defined.
Source code:
namespace eval ::ns::stats {}
::nsf::object::alloc ::nx::Class ::ns::stats::Collector {}
::ns::stats::Collector public method cpuCapacity {} {
        #
        # Inspect host logical CPUs, process affinity and visible cgroup v1/v2
        # CPU quotas, cpusets and throttling counters. effectiveCores is the minimum
        # known ceiling, not reserved or guaranteed CPU capacity. Missing probes
        # remain unknown and are reflected in constraint completeness and reasons.
        # Counters can include other processes or sibling cgroups; do not sum
        # ancestor counters as independent process usage. Does not change limits.
        #
        # @return Dictionary of visible CPU ceilings, effectiveCores, constraint completeness,
        #     cumulative cgroup throttling counters and limitations; unavailable values are
        #     empty.
        #
        set result {hostLogicalCpus {} affinityCpus {} cpusetCpus {} quotaCores {} effectiveCores {} quotaStatus unknown constraintsComplete false throttlingAvailable false throttlingScope cgroup-hierarchy throttling {} limitations {Visible CPU ceilings are not reserved capacity. Hidden ancestor limits, host contention and CPU shares can reduce availability. Throttling counters are cumulative for each visible cgroup, including other processes; ancestor counters can include sibling groups.}}
        set reasons {}; set ceilings {}
        if {![catch {:cpuListCount [:systemText /sys/devices/system/cpu/online]} count] && $count>0} {
            dict set result hostLogicalCpus $count; lappend ceilings $count
        }
        if {![catch {:systemText /proc/self/status} status]
            && [regexp -line {^Cpus_allowed_list:\s*([^\n]+)} $status -> list]
            && ![catch {:cpuListCount [string trim $list]} count] && $count>0} {
            dict set result affinityCpus $count; lappend ceilings $count
        } else {lappend reasons affinity-unavailable}
        set memberships {}
        if {![catch {:systemText /proc/self/cgroup} text]} {
            foreach line [split $text \n] {
                if {![regexp {^[0-9]+:([^:]*):(.*)$} $line -> controllers path]} continue
                if {$controllers eq ""} {dict set memberships unified $path}
                foreach controller [split $controllers ,] {dict set memberships $controller $path}
            }
        }
        set quotaSeen false; set quotaMissing false; set hierarchySeen false
        set groups {}; set visited {}
        if {![catch {:systemText /proc/self/mountinfo} mounts]} {
            foreach line [split $mounts \n] {
                set parts [split $line " "]
                set separator [lsearch -exact $parts -]
                if {$separator<6} continue
                set type [lindex $parts [expr {$separator+1}]]
                if {$type ni {cgroup cgroup2}} continue
                set root [:mountPath [lindex $parts 3]]
                set mount [:mountPath [lindex $parts 4]]
                set controllers [split [lindex $parts [expr {$separator+3}]] ,]
                set key {}; set cpu false; set cpuset false
                if {$type eq "cgroup2" && [dict exists $memberships unified]} {
                    set key unified; set cpu true; set cpuset true
                } else {
                    foreach controller {cpu cpuset} {
                        if {$controller in $controllers && [dict exists $memberships $controller]} {
                            set key $controller
                            if {$controller eq "cpu"} {set cpu true} else {set cpuset true}
                        }
                    }
                }
                if {$key eq ""} continue
                set member [dict get $memberships $key]
                # Do not traverse outside a visible mount, including namespace
                # membership paths containing parent components.
                if {".." in [file split $member]} {lappend reasons cgroup-path-unresolvable; continue}
                if {$root eq "/"} {set relative [string trimleft $member /]
                } elseif {$member eq "/" || $member eq $root} {set relative {}
                } elseif {[string first "$root/" $member]==0} {set relative [string range $member [expr {[string length $root]+1}] end]
                } else {continue}
                set directory [file join $mount $relative]
                set hierarchySeen true
                while {1} {
                    if {[lsearch -exact $visited [list $directory $cpu $cpuset]]<0} {
                        lappend visited [list $directory $cpu $cpuset]
                        if {$cpu} {
                            if {$type eq "cgroup2"} {
                                set readable [expr {![catch {:systemText $directory/cpu.max} maximum]}]
                                if {$readable && [regexp {^(max|[0-9]+) ([1-9][0-9]*)$} $maximum -> quota period]} {
                                    set quotaSeen true
                                    if {$quota ne "max" && $quota>0} {lappend ceilings [expr {double($quota)/$period}]; set q [expr {double($quota)/$period}]
                                        if {[dict get $result quotaCores] eq "" || $q<[dict get $result quotaCores]} {dict set result quotaCores $q}}
                                } elseif {$directory ne $mount} {set quotaMissing true}
                            } else {
                                if {![catch {:systemText $directory/cpu.cfs_quota_us} quota]
                                    && ![catch {:systemText $directory/cpu.cfs_period_us} period]
                                    && [string is entier -strict $quota] && [string is entier -strict $period] && $period>0} {
                                    set quotaSeen true
                                    if {$quota>0} {set q [expr {double($quota)/$period}]; lappend ceilings $q
                                        if {[dict get $result quotaCores] eq "" || $q<[dict get $result quotaCores]} {dict set result quotaCores $q}}
                                } else {set quotaMissing true}
                            }
                            if {![catch {:systemText $directory/cpu.stat} counters]} {
                                set row [dict create path $directory]
                                foreach counter [split $counters \n] {
                                    lassign $counter name value
                                    if {![string is entier -strict $value] || $value<0} continue
                                    switch -- $name {
                                        nr_periods {dict set row periods $value}
                                        nr_throttled {dict set row throttledPeriods $value}
                                        throttled_usec {dict set row throttledSeconds [expr {$value/1000000.0}]}
                                        throttled_time {dict set row throttledSeconds [expr {$value/1000000000.0}]}
                                    }
                                }
                                if {[dict size $row]>1} {dict set groups $directory $row}
                            }
                        }
                        if {$cpuset} {
                            set file [expr {$type eq "cgroup2" ? "cpuset.cpus.effective" : "cpuset.cpus"}]
                            if {![catch {:systemText $directory/$file} list] && $list ne "" && ![catch {:cpuListCount $list} count] && $count>0} {
                                lappend ceilings $count
                                if {[dict get $result cpusetCpus] eq "" || $count<[dict get $result cpusetCpus]} {dict set result cpusetCpus $count}
                            }
                        }
                    }
                    if {$directory eq $mount} break
                    set directory [file dirname $directory]
                }
            }
        }
        if {!$hierarchySeen} {lappend reasons cgroup-hierarchy-unavailable}
        if {!$quotaSeen || $quotaMissing} {lappend reasons quota-unavailable-or-partial}
        dict set result quotaStatus [expr {[dict get $result quotaCores] ne "" ? "limited" : ($quotaSeen && !$quotaMissing ? "unlimited" : "unknown")}]
        if {[llength $ceilings]} {dict set result effectiveCores [lindex [lsort -real $ceilings] 0]}
        dict set result constraintsComplete [expr {$reasons eq ""}]
        dict set result incompleteReasons [lsort -unique $reasons]
        dict set result throttling $groups
        dict set result throttlingAvailable [expr {[dict size $groups]>0}]
        return $result
    }
::ns::stats::Collector protected method cpuListCount text {
        set ranges {}
        foreach item [split $text ,] {
            if {![regexp {^([0-9]+)(?:-([0-9]+))?$} [string trim $item] -> first last]} {error "Invalid CPU list"}
            if {$last eq ""} {set last $first}
            if {$last<$first} {error "Invalid CPU range"}
            lappend ranges [list $first $last]
        }
        set count 0; set end -1
        foreach range [lsort -integer -index 0 $ranges] {
            lassign $range first last
            if {$last>$end} {incr count [expr {$last-max($first,$end+1)+1}]; set end $last}
        }
        return $count
    }
::ns::stats::Collector public method describe {{-server {}}} {
        #
        # Return a dictionary describing the NaviServer process, virtual
        # servers, connection pools and network drivers. An optional server
        # argument restricts virtual-server data; process-wide information
        # retains its process scope. Availability fields identify optional
        # information that could not be collected.
        #
        # Does not start sampling or modify the sampling baseline.
        #
        # @param server Optional virtual-server name; empty selects all configured servers.
        #     Process and driver information remain process-wide.
        # @return Topology dictionary with schemaVersion, scope, process, servers, drivers and
        #     availability information.
        #
        set result [dict create schemaVersion 1 scope process  process [dict create  pid [ns_info pid]  bootTime [ns_info boottime]  version [ns_info patchlevel]  host [ns_info hostname]  uptimeSeconds [ns_info uptime]]  servers {} drivers {} availability {}]

        if {![catch {ns_info buildinfo} build]} {
            dict set result process build $build
        }
        dict set result process platform [dict create  os $::tcl_platform(os)  osVersion $::tcl_platform(osVersion)  machine $::tcl_platform(machine)]
        dict set result process cpuCapacity [:cpuCapacity]
        foreach s [ns_info servers] {
            set pools {}
            foreach p [ns_server -server $s pools] {
                set name [expr {$p eq "" ? "default" : $p}]
                set threads [ns_server -server $s -pool $p threads]
                set limits {}
                foreach k {min max maxconnections} {
                    if {[dict exists $threads $k]} {
                        dict set limits $k [dict get $threads $k]
                    }
                }
                dict set pools $name $limits
            }
            set entry [dict create  pools $pools  databasePools [ns_config ns/server/$s/db pools {}] bindings {} availability {}]
            if {![catch {ns_server -server $s modules} modules]} {
                dict set entry modules $modules
                dict set entry availability modules true
            } else {
                dict set entry availability modules false
            }
            if {![catch {ns_driver info} infos]} {
                foreach driver $infos {
                    set module [dict get $driver module]
                    set mappings {}
                    set allMappings true
                    if {[catch {ns_config -all ns/module/$module/servers $s {}} mappings]} {
                        set mappings [ns_config ns/module/$module/servers $s {}]
                        set allMappings false
                    }
                    if {$mappings ne "" || [dict get $driver server] eq $s} {
                        dict set entry bindings $module [dict create  address [dict get $driver address]  port [dict get $driver port]  protocol [dict get $driver protocol]  hostMappings $mappings  hostMappingsComplete $allMappings]
                    }
                }
            }
            dict set result servers $s $entry
        }

        if {![catch {ns_driver info} info]} {
            set drivers {}
            foreach driver $info {
                set entry {}
                foreach k {
                    module type server location address port defaultport protocol
                    sendwait recvwait recvbufsize sendbufsize libraryversion
                } {
                    if {[dict exists $driver $k]} {
                        dict set entry $k [dict get $driver $k]
                    }
                }
                lappend drivers $entry
            }
            dict set result drivers $drivers
            dict set result availability drivers true
        } else {
            dict set result availability drivers false
        }
        return [:selectServer $result $server]
    }
::ns::stats::Collector protected method mountPath value {
        return [string map [list {\040} " " {\011} "\t" {\134} "\\"] $value]
    }
::ns::stats::Collector protected method selectServer {record server {validate true}} {
        # Restrict virtual-server data; historical records may refer to removed servers.
        if {$server ne ""} {
            if {$validate && $server ni [ns_info servers]} {
                error "unknown server: $server"
            }
            set selected {}
            if {[dict exists $record servers $server]} {
                dict set selected $server [dict get $record servers $server]
            }
            dict set record servers $selected
        }
        return $record
    }
::ns::stats::Collector public method snapshot {{-server {}}} {
        #
        # Collect current gauges, cumulative counters, allocator statistics
        # and thread CPU times into a timestamped dictionary. The timestamp
        # is in epoch microseconds; collectionSeconds records collection
        # duration. The observations are collected sequentially and do not
        # constitute an atomic snapshot of the entire process.
        #
        # An optional server argument restricts virtual-server data.
        # Does not publish a sample or advance the sampling baseline.
        #
        # @param server Optional virtual-server name; empty selects all configured servers.
        #     Process metrics remain process-wide.
        # @return Timestamped dictionary of gauges, cumulative counters, thread CPU counters,
        #     units and availability; timestamp is in epoch microseconds.
        #

        set start [clock microseconds]
        set result [dict create  schemaVersion 1  scope process  process [dict create pid [pid] bootTime [ns_info boottime]]  drivers {}  servers {}  cpu {}  availability {}  units {time seconds cpuCounter microseconds cpuPercent percentOfOneCore}]
        dict set result process cpuCapacity [:cpuCapacity]
        if {![catch {ns_driver stats} stats]} {
            foreach entry $stats {
                set counters {}
                foreach key {received partial spooled errors} {
                    dict set counters $key [dict get $entry $key]
                }
                set socketStates [dict create  reading [dict get $entry reading]  waitingForConnection [dict get $entry waiting]  closing [dict get $entry closing]]
                dict set result drivers [dict get $entry thread]  [dict create thread [dict get $entry thread]  module [dict get $entry module]  counters $counters socketStates $socketStates]
            }
            dict set result availability drivers true
        } else {
            dict set result availability drivers false
        }
        foreach s [ns_info servers] {
            foreach p [ns_server -server $s pools] {
                set name [expr {$p eq "" ? "default" : $p}]
                dict set result servers $s pools $name  [dict create  counters [ns_server -server $s -pool $p stats]  gauges [dict create  threads [ns_server -server $s -pool $p threads]  waitingRequests [ns_server -server $s -pool $p waiting]  activeRequests [llength [ns_server -server $s -pool $p active]]]]
            }
        }

        set cpu {}
        if {![catch {ns_info threadcputimes} times]} {
            foreach t [ns_info threads] {
                set tid [lindex $t 7]
                if {$tid eq "" || ![dict exists $times $tid]} {
                    continue
                }
                set identity [list $tid [lindex $t 4]]
                dict set cpu $identity [dict merge [dict get $times $tid] [dict create name [lindex $t 0]]]
            }
        }
        set memory {}

        if {![catch {ns_info meminfo} info]} {
            foreach k {
                current_allocated_bytes heap_size total_physical_bytes slack_bytes
                pageheap_free_bytes pageheap_unmapped_bytes pageheap_committed_bytes
                central_cache_free_bytes transfer_cache_free_bytes thread_cache_free_bytes
                current_total_thread_cache_bytes
            } {
                if {[dict exists $info $k]} {
                    dict set memory $k [dict get $info $k]
                }
            }
        }
        dict set result process allocatorBytes $memory
        dict set result availability allocatorMemory [expr {$memory ne ""}]
        dict set result cpu $cpu
        dict set result availability cpu [expr {$cpu ne ""}]
        if {![catch {ns_db stats} dbstats]} {
            dict set result databasePools $dbstats
            dict set result availability databasePools true
        } else {
            dict set result availability databasePools false
        }
        dict set result timestamp [clock microseconds]
        dict set result collectionSeconds [expr {([dict get $result timestamp]-$start)/1000000.0}]
        return [:selectServer $result $server]
    }
::ns::stats::Collector protected method systemText path {
        set f [open $path r]
        try {return [string trim [read $f 1048576]]} finally {close $f}
    }
XQL Not present:
Generic, PostgreSQL, Oracle
[ hide source ] | [ make this the default ]
Show another procedure: