1# counter.tcl --
2#
3#   Procedures to manage simple counters and histograms.
4#
5# Copyright (c) 1998-2000 by Ajuba Solutions.
6#
7# See the file "license.terms" for information on usage and redistribution
8# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
9#
10# RCS: @(#) $Id: counter.tcl,v 1.23 2005/09/30 05:36:38 andreas_kupries Exp $
11
12package require Tcl 8.2
13
14namespace eval ::counter {
15
16    # Variables of name counter::T-$tagname
17    # are created as arrays to support each counter.
18
19    # Time-based histograms are kept in sync with each other,
20    # so these variables are shared among them.
21    # These base times record the time corresponding to the first bucket
22    # of the per-minute, per-hour, and per-day time-based histograms.
23
24    variable startTime
25    variable minuteBase
26    variable hourBase
27    variable hourEnd
28    variable dayBase
29    variable hourIndex
30    variable dayIndex
31
32    # The time-based histogram uses an after event and a list
33    # of counters to do mergeing on.
34
35    variable tagsToMerge
36    if {![info exists tagsToMerge]} {
37    set tagsToMerge {}
38    }
39    variable mergeInterval
40
41    namespace export init reset count exists get names start stop
42    namespace export histHtmlDisplay histHtmlDisplayRow histHtmlDisplayBarChart
43}
44
45# ::counter::init --
46#
47#   Set up a counter.
48#
49# Arguments:
50#   tag The identifier for the counter.  Pass this to counter::count
51#   args    option values pairs that define characteristics of the counter:
52#       See the man page for definitons.
53#
54# Results:
55#   None.
56#
57# Side Effects:
58#   Initializes state about a counter.
59
60proc ::counter::init {tag args} {
61    upvar #0 counter::T-$tag counter
62    if {[info exists counter]} {
63    unset counter
64    }
65    set counter(N) 0    ;# Number of samples
66    set counter(total) 0
67    set counter(type) {}
68
69    # With an empty type the counter is a simple accumulator
70    # for which we can compute an average.  Here we loop through
71    # the args to determine what additional counter attributes
72    # we need to maintain in counter::count
73
74    foreach {option value} $args {
75    switch -- $option {
76        -timehist {
77        variable tagsToMerge
78        variable secsPerMinute
79        variable startTime
80        variable minuteBase
81        variable hourBase
82        variable dayBase
83        variable hourIndex
84        variable dayIndex
85
86        upvar #0 counter::H-$tag histogram
87        upvar #0 counter::Hour-$tag hourhist
88        upvar #0 counter::Day-$tag dayhist
89
90        # Clear the histograms.
91
92        for {set i 0} {$i < 60} {incr i} {
93            set histogram($i) 0
94        }
95        for {set i 0} {$i < 24} {incr i} {
96            set hourhist($i) 0
97        }
98        if {[info exists dayhist]} {
99            unset dayhist
100        }
101        set dayhist(0) 0
102
103        # Clear all-time high records
104
105        set counter(maxPerMinute) 0
106        set counter(maxPerHour) 0
107        set counter(maxPerDay) 0
108
109        # The value associated with -timehist is the number of seconds
110        # in each bucket.  Normally this is 60, but for
111        # testing, we compress minutes.  The value is limited at
112        # 60 because the per-minute buckets are accumulated into
113        # per-hour buckets later.
114
115        if {$value == "" || $value == 0 || $value > 60} {
116            set value 60
117        }
118
119        # Histogram state variables.
120        # All time-base histograms share the same bucket size
121        # and starting times to keep them all synchronized.
122        # So, we only initialize these parameters once.
123
124        if {![info exists secsPerMinute]} {
125            set secsPerMinute $value
126
127            set startTime [clock seconds]
128            set dayIndex 0
129
130            set dayStart [clock scan [clock format $startTime \
131                -format 00:00]]
132
133            # Figure out what "hour" we are
134
135            set delta [expr {$startTime - $dayStart}]
136            set hourIndex [expr {$delta / ($secsPerMinute * 60)}]
137            set day [expr {$hourIndex / 24}]
138            set hourIndex [expr {$hourIndex % 24}]
139
140            set hourBase [expr {$dayStart + $day * $secsPerMinute * 60 * 24}]
141            set minuteBase [expr {$hourBase + $hourIndex * 60 * $secsPerMinute}]
142
143            set partialHour [expr {$startTime -
144            ($hourBase + $hourIndex * 60 * $secsPerMinute)}]
145            set secs [expr {(60 * $secsPerMinute) - $partialHour}]
146            if {$secs <= 0} {
147            set secs 1
148            }
149
150            # After the first timer, the event occurs once each "hour"
151
152            set mergeInterval [expr {60 * $secsPerMinute * 1000}]
153            after [expr {$secs * 1000}] [list counter::MergeHour $mergeInterval]
154        }
155        if {[lsearch $tagsToMerge $tag] < 0} {
156            lappend tagsToMerge $tag
157        }
158
159        # This records the last used slots in order to zero-out the
160        # buckets that are skipped during idle periods.
161
162        set counter(lastMinute) -1
163
164        # The following is referenced when bugs cause histogram
165        # hits outside the expect range (overflow and underflow)
166
167        set counter(bucketsize)  0
168        }
169        -group {
170        # Cluster a set of counters with a single total
171
172        upvar #0 counter::H-$tag histogram
173        if {[info exists histogram]} {
174            unset histogram
175        }
176        set counter(group) $value
177        }
178        -lastn {
179        # The lastN samples are kept if a vector to form a running average.
180
181        upvar #0 counter::V-$tag vector
182        set counter(lastn) $value
183        set counter(index) 0
184        if {[info exists vector]} {
185            unset vector
186        }
187        for {set i 0} {$i < $value} {incr i} {
188            set vector($i) 0
189        }
190        }
191        -hist {
192        # A value-based histogram with buckets for different values.
193
194        upvar #0 counter::H-$tag histogram
195        if {[info exists histogram]} {
196            unset histogram
197        }
198        set counter(bucketsize) $value
199        set counter(mult) 1
200        }
201        -hist2x {
202        upvar #0 counter::H-$tag histogram
203        if {[info exists histogram]} {
204            unset histogram
205        }
206        set counter(bucketsize) $value
207        set counter(mult) 2
208        }
209        -hist10x {
210        upvar #0 counter::H-$tag histogram
211        if {[info exists histogram]} {
212            unset histogram
213        }
214        set counter(bucketsize) $value
215        set counter(mult) 10
216        }
217        -histlog {
218        upvar #0 counter::H-$tag histogram
219        if {[info exists histogram]} {
220            unset histogram
221        }
222        set counter(bucketsize) $value
223        }
224        -simple {
225        # Useful when disabling predefined -timehist or -group counter
226        }
227        default {
228        return -code error "Unsupported option $option.\
229        Must be -timehist, -group, -lastn, -hist, -hist2x, -hist10x, -histlog, or -simple."
230        }
231    }
232    if {[string length $option]} {
233        # In case an option doesn't change the type, but
234        # this feature of the interface isn't used, etc.
235
236        lappend counter(type) $option
237    }
238    }
239
240    # Instead of supporting a counter that could have multiple attributes,
241    # we support a single type to make counting more efficient.
242
243    if {[llength $counter(type)] > 1} {
244    return -code error "Multiple type attributes not supported.  Use only one of\
245        -timehist, -group, -lastn, -hist, -hist2x, -hist10x, -histlog, -disabled."
246    }
247    return ""
248}
249
250# ::counter::reset --
251#
252#   Reset a counter.
253#
254# Arguments:
255#   tag The identifier for the counter.
256#
257# Results:
258#   None.
259#
260# Side Effects:
261#   Deletes the counter and calls counter::init again for it.
262
263proc ::counter::reset {tag args} {
264    upvar #0 counter::T-$tag counter
265
266    # Layer reset on top of init.  Here we figure out what
267    # we need to pass into the init procedure to recreate it.
268
269    switch -- $counter(type) {
270    ""  {
271        set args ""
272    }
273    -group {
274        upvar #0 counter::H-$tag histogram
275        if {[info exists histogram]} {
276        unset histogram
277        }
278        set args [list -group $counter(group)]
279    }
280    -lastn {
281        upvar #0 counter::V-$tag vector
282        if {[info exists vector]} {
283        unset vector
284        }
285        set args [list -lastn $counter(lastn)]
286    }
287    -hist -
288    -hist10x -
289    -histlog -
290    -hist2x {
291        upvar #0 counter::H-$tag histogram
292        if {[info exists histogram]} {
293        unset histogram
294        }
295        set args [list $counter(type) $counter(bucketsize)]
296    }
297    -timehist {
298        foreach h [list counter::H-$tag counter::Hour-$tag counter::Day-$tag] {
299        upvar #0 $h histogram
300        if {[info exists histogram]} {
301            unset histogram
302        }
303        }
304        set args [list -timehist $counter::secsPerMinute]
305    }
306    default {#ignore}
307    }
308    unset counter
309    eval {counter::init $tag} $args
310    set counter(resetDate) [clock seconds]
311    return ""
312}
313
314# ::counter::count --
315#
316#   Accumulate statistics.
317#
318# Arguments:
319#   tag The counter identifier.
320#   delta   The increment amount.  Defaults to 1.
321#   arg For -group types, this is the histogram index.
322#
323# Results:
324#   None
325#
326# Side Effects:
327#   Accumlate statistics.
328
329proc ::counter::count {tag {delta 1} args} {
330    upvar #0 counter::T-$tag counter
331    set counter(total) [expr {$counter(total) + $delta}]
332    incr counter(N)
333
334    # Instead of supporting a counter that could have multiple attributes,
335    # we support a single type to make counting a skosh more efficient.
336
337#    foreach option $counter(type) {
338    switch -- $counter(type) {
339        ""  {
340        # Simple counter
341        return
342        }
343        -group {
344        upvar #0 counter::H-$tag histogram
345        set subIndex [lindex $args 0]
346        if {![info exists histogram($subIndex)]} {
347            set histogram($subIndex) 0
348        }
349        set histogram($subIndex) [expr {$histogram($subIndex) + $delta}]
350        }
351        -lastn {
352        upvar #0 counter::V-$tag vector
353        set vector($counter(index)) $delta
354        set counter(index) [expr {($counter(index) +1)%$counter(lastn)}]
355        }
356        -hist {
357        upvar #0 counter::H-$tag histogram
358        set bucket [expr {int($delta / $counter(bucketsize))}]
359        if {![info exists histogram($bucket)]} {
360            set histogram($bucket) 0
361        }
362        incr histogram($bucket)
363        }
364        -hist10x -
365        -hist2x {
366        upvar #0 counter::H-$tag histogram
367        set bucket 0
368        for {set max $counter(bucketsize)} {$delta > $max} \
369            {set max [expr {$max * $counter(mult)}]} {
370            incr bucket
371        }
372        if {![info exists histogram($bucket)]} {
373            set histogram($bucket) 0
374        }
375        incr histogram($bucket)
376        }
377        -histlog {
378        upvar #0 counter::H-$tag histogram
379        set bucket [expr {int(log($delta)*$counter(bucketsize))}]
380        if {![info exists histogram($bucket)]} {
381            set histogram($bucket) 0
382        }
383        incr histogram($bucket)
384        }
385        -timehist {
386        upvar #0 counter::H-$tag histogram
387        variable minuteBase
388        variable secsPerMinute
389
390        set minute [expr {([clock seconds] - $minuteBase) / $secsPerMinute}]
391        if {$minute > 59} {
392            # this occurs while debugging if the process is
393            # stopped at a breakpoint too long.
394            set minute 59
395        }
396
397        # Initialize the current bucket and
398        # clear any buckets we've skipped since the last sample.
399
400        if {$minute != $counter(lastMinute)} {
401            set histogram($minute) 0
402            for {set i [expr {$counter(lastMinute)+1}]} \
403                {$i < $minute} \
404                {incr i} {
405            set histogram($i) 0
406            }
407            set counter(lastMinute) $minute
408        }
409        set histogram($minute) [expr {$histogram($minute) + $delta}]
410        }
411        default {#ignore}
412    }
413#   }
414    return
415}
416
417# ::counter::exists --
418#
419#   Return true if the counter exists.
420#
421# Arguments:
422#   tag The counter identifier.
423#
424# Results:
425#   1 if it has been defined.
426#
427# Side Effects:
428#   None.
429
430proc ::counter::exists {tag} {
431    upvar #0 counter::T-$tag counter
432    return [info exists counter]
433}
434
435# ::counter::get --
436#
437#   Return statistics.
438#
439# Arguments:
440#   tag The counter identifier.
441#   option  What statistic to get
442#   args    Needed by some options.
443#
444# Results:
445#   With no args, just the counter value.
446#
447# Side Effects:
448#   None.
449
450proc ::counter::get {tag {option -total} args} {
451    upvar #0 counter::T-$tag counter
452    switch -- $option {
453    -total {
454        return $counter(total)
455    }
456    -totalVar {
457        return ::counter::T-$tag\(total)
458    }
459    -N {
460        return $counter(N)
461    }
462    -avg {
463        if {$counter(N) == 0} {
464        return 0
465        } else {
466        return [expr {$counter(total) / double($counter(N))}]
467        }
468    }
469    -avgn {
470        if {$counter(type) != "-lastn"} {
471        return -code error "The -avgn option is only supported for -lastn counters."
472        }
473        upvar #0 counter::V-$tag vector
474        set sum 0
475        for {set i 0} {($i < $counter(N)) && ($i < $counter(lastn))} {incr i} {
476        set sum [expr {$sum + $vector($i)}]
477        }
478        if {$i == 0} {
479        return 0
480        } else {
481        return [expr {$sum / double($i)}]
482        }
483    }
484    -hist {
485        upvar #0 counter::H-$tag histogram
486        if {[llength $args]} {
487        # Return particular bucket
488        set bucket [lindex $args 0]
489        if {[info exists histogram($bucket)]} {
490            return $histogram($bucket)
491        } else {
492            return 0
493        }
494        } else {
495        # Dump the whole histogram
496
497        set result {}
498        if {$counter(type) == "-group"} {
499            set sort -dictionary
500        } else {
501            set sort -integer
502        }
503        foreach x [lsort $sort [array names histogram]] {
504            lappend result $x $histogram($x)
505        }
506        return $result
507        }
508    }
509    -histVar {
510        return ::counter::H-$tag
511    }
512    -histHour {
513        upvar #0 counter::Hour-$tag histogram
514        set result {}
515        foreach x [lsort -integer [array names histogram]] {
516        lappend result $x $histogram($x)
517        }
518        return $result
519    }
520    -histHourVar {
521        return ::counter::Hour-$tag
522    }
523    -histDay {
524        upvar #0 counter::Day-$tag histogram
525        set result {}
526        foreach x [lsort -integer [array names histogram]] {
527        lappend result $x $histogram($x)
528        }
529        return $result
530    }
531    -histDayVar {
532        return ::counter::Day-$tag
533    }
534    -maxPerMinute {
535        return $counter(maxPerMinute)
536    }
537    -maxPerHour {
538        return $counter(maxPerHour)
539    }
540    -maxPerDay {
541        return $counter(maxPerDay)
542    }
543    -resetDate {
544        if {[info exists counter(resetDate)]} {
545        return $counter(resetDate)
546        } else {
547        return ""
548        }
549    }
550    -all {
551        return [array get counter]
552    }
553    default {
554        return -code error "Invalid option $option.\
555        Should be -all, -total, -N, -avg, -avgn, -hist, -histHour,\
556        -histDay, -totalVar, -histVar, -histHourVar, -histDayVar -resetDate."
557    }
558    }
559}
560
561# ::counter::names --
562#
563#   Return the list of defined counters.
564#
565# Arguments:
566#   none
567#
568# Results:
569#   A list of counter tags.
570#
571# Side Effects:
572#   None.
573
574proc ::counter::names {} {
575    set result {}
576    foreach v [info vars ::counter::T-*] {
577    if {[info exists $v]} {
578        # Declared arrays might not exist, yet
579        # strip prefix from name
580        set v [string range $v [string length "::counter::T-"] end]
581        lappend result $v
582    }
583    }
584    return $result
585}
586
587# ::counter::MergeHour --
588#
589#   Sum the per-minute histogram into the next hourly bucket.
590#   On 24-hour boundaries, sum the hourly buckets into the next day bucket.
591#   This operates on all time-based histograms.
592#
593# Arguments:
594#   none
595#
596# Results:
597#   none
598#
599# Side Effects:
600#   See description.
601
602proc ::counter::MergeHour {interval} {
603    variable hourIndex
604    variable minuteBase
605    variable hourBase
606    variable tagsToMerge
607    variable secsPerMinute
608
609    after $interval [list counter::MergeHour $interval]
610    if {![info exists hourBase] || $hourIndex == 0} {
611    set hourBase $minuteBase
612    }
613    set minuteBase [clock seconds]
614
615    foreach tag $tagsToMerge {
616    upvar #0 counter::T-$tag counter
617    upvar #0 counter::H-$tag histogram
618    upvar #0 counter::Hour-$tag hourhist
619
620    # Clear any buckets we've skipped since the last sample.
621
622    for {set i [expr {$counter(lastMinute)+1}]} {$i < 60} {incr i} {
623        set histogram($i) 0
624    }
625    set counter(lastMinute) -1
626
627    # Accumulate into the next hour bucket.
628
629    set hourhist($hourIndex) 0
630    set max 0
631    foreach i [array names histogram] {
632        set hourhist($hourIndex) [expr {$hourhist($hourIndex) + $histogram($i)}]
633        if {$histogram($i) > $max} {
634        set max $histogram($i)
635        }
636    }
637    set perSec [expr {$max / $secsPerMinute}]
638    if {$perSec > $counter(maxPerMinute)} {
639        set counter(maxPerMinute) $perSec
640    }
641    }
642    set hourIndex [expr {($hourIndex + 1) % 24}]
643    if {$hourIndex == 0} {
644    counter::MergeDay
645    }
646
647}
648# ::counter::MergeDay --
649#
650#   Sum the per-minute histogram into the next hourly bucket.
651#   On 24-hour boundaries, sum the hourly buckets into the next day bucket.
652#   This operates on all time-based histograms.
653#
654# Arguments:
655#   none
656#
657# Results:
658#   none
659#
660# Side Effects:
661#   See description.
662
663proc ::counter::MergeDay {} {
664    variable dayIndex
665    variable dayBase
666    variable hourBase
667    variable tagsToMerge
668    variable secsPerMinute
669
670    # Save the hours histogram into a bucket for the last day
671    # counter(day,$day) is the starting time for that day bucket
672
673    if {![info exists dayBase]} {
674    set dayBase $hourBase
675    }
676    foreach tag $tagsToMerge {
677    upvar #0 counter::T-$tag counter
678    upvar #0 counter::Day-$tag dayhist
679    upvar #0 counter::Hour-$tag hourhist
680    set dayhist($dayIndex) 0
681    set max 0
682    for {set i 0} {$i < 24} {incr i} {
683        if {[info exists hourhist($i)]} {
684        set dayhist($dayIndex) [expr {$dayhist($dayIndex) + $hourhist($i)}]
685        if {$hourhist($i) > $max} {
686            set max $hourhist($i)
687        }
688        }
689    }
690    set perSec [expr {double($max) / ($secsPerMinute * 60)}]
691    if {$perSec > $counter(maxPerHour)} {
692        set counter(maxPerHour) $perSec
693    }
694    }
695    set perSec [expr {double($dayhist($dayIndex)) / ($secsPerMinute * 60 * 24)}]
696    if {$perSec > $counter(maxPerDay)} {
697    set counter(maxPerDay) $perSec
698    }
699    incr dayIndex
700}
701
702# ::counter::histHtmlDisplay --
703#
704#   Create an html display of the histogram.
705#
706# Arguments:
707#   tag The counter tag
708#   args    option, value pairs that affect the display:
709#       -title  Label to display above bar chart
710#       -unit   minutes, hours, or days select time-base histograms.
711#           Specify anything else for value-based histograms.
712#       -images URL of /images directory.
713#       -gif    Image for normal histogram bars
714#       -ongif  Image for the active histogram bar
715#       -max    Maximum number of value-based buckets to display
716#       -height Pixel height of the highest bar
717#       -width  Pixel width of each bar
718#       -skip   Buckets to skip when labeling value-based histograms
719#       -format Format used to display labels of buckets.
720#       -text   If 1, a text version of the histogram is dumped,
721#           otherwise a graphical one is generated.
722#
723# Results:
724#   HTML for the display as a complete table.
725#
726# Side Effects:
727#   None.
728
729proc ::counter::histHtmlDisplay {tag args} {
730    append result "<p>\n<table border=0 cellpadding=0 cellspacing=0>\n"
731    append result [eval {counter::histHtmlDisplayRow $tag} $args]
732    append result </table>
733    return $result
734}
735
736# ::counter::histHtmlDisplayRow --
737#
738#   Create an html display of the histogram.
739#
740# Arguments:
741#   See counter::histHtmlDisplay
742#
743# Results:
744#   HTML for the display.  Ths is one row of a 2-column table,
745#   the calling page must define the <table> tag.
746#
747# Side Effects:
748#   None.
749
750proc ::counter::histHtmlDisplayRow {tag args} {
751    upvar #0 counter::T-$tag counter
752    variable secsPerMinute
753    variable minuteBase
754    variable hourBase
755    variable dayBase
756    variable hourIndex
757    variable dayIndex
758
759    array set options [list \
760    -title  $tag \
761    -unit   "" \
762    -images /images \
763    -gif    Blue.gif \
764    -ongif  Red.gif \
765    -max    -1 \
766    -height 100 \
767    -width  4 \
768    -skip   4 \
769    -format %.2f \
770    -text   0
771    ]
772    array set options $args
773
774    # Support for self-posting pages that can clear counters.
775
776    append result "<!-- resetCounter [ncgi::value resetCounter] -->"
777    if {[ncgi::value resetCounter] == $tag} {
778    counter::reset $tag
779    return "<!-- Reset $tag counter -->"
780    }
781
782    switch -glob -- $options(-unit) {
783    min* {
784        upvar #0 counter::H-$tag histogram
785        set histname counter::H-$tag
786        if {![info exists minuteBase]} {
787        return "<!-- No time-based histograms defined -->"
788        }
789        set time $minuteBase
790        set secsForMax $secsPerMinute
791        set periodMax $counter(maxPerMinute)
792        set curIndex [expr {([clock seconds] - $minuteBase) / $secsPerMinute}]
793        set options(-max) 60
794        set options(-min) 0
795    }
796    hour* {
797        upvar #0 counter::Hour-$tag histogram
798        set histname counter::Hour-$tag
799        if {![info exists hourBase]} {
800        return "<!-- Hour merge has not occurred -->"
801        }
802        set time $hourBase
803        set secsForMax [expr {$secsPerMinute * 60}]
804        set periodMax $counter(maxPerHour)
805        set curIndex [expr {$hourIndex - 1}]
806        if {$curIndex < 0} {
807        set curIndex 23
808        }
809        set options(-max) 24
810        set options(-min) 0
811    }
812    day* {
813        upvar #0 counter::Day-$tag histogram
814        set histname counter::Day-$tag
815        if {![info exists dayBase]} {
816        return "<!-- Hour merge has not occurred -->"
817        }
818        set time $dayBase
819        set secsForMax [expr {$secsPerMinute * 60 * 24}]
820        set periodMax $counter(maxPerDay)
821        set curIndex dayIndex
822        set options(-max) $dayIndex
823        set options(-min) 0
824    }
825    default {
826        # Value-based histogram with arbitrary units.
827
828        upvar #0 counter::H-$tag histogram
829        set histname counter::H-$tag
830
831        set unit $options(-unit)
832        set curIndex ""
833        set time ""
834    }
835    }
836    if {! [info exists histogram]} {
837    return "<!-- $histname doesn't exist -->\n"
838    }
839
840    set max 0
841    set maxName 0
842    foreach {name value} [array get histogram] {
843    if {$value > $max} {
844        set max $value
845        set maxName $name
846    }
847    }
848
849    # Start 2-column HTML display.  A summary table at the left, the histogram on the right.
850
851    append result "<tr><td valign=top>\n"
852
853    append result "<table bgcolor=#EEEEEE>\n"
854    append result "<tr><td colspan=2 align=center>[html::font]<b>$options(-title)</b></font></td></tr>\n"
855    append result "<tr><td>[html::font]<b>Total</b></font></td>"
856    append result "<td>[html::font][format $options(-format) $counter(total)]</font></td></tr>\n"
857
858    if {[info exists secsForMax]} {
859
860    # Time-base histogram
861
862    set string {}
863    set t $secsForMax
864    set days [expr {$t / (60 * 60 * 24)}]
865    if {$days == 1} {
866        append string "1 Day "
867    } elseif {$days > 1} {
868        append string "$days Days "
869    }
870    set t [expr {$t - $days * (60 * 60 * 24)}]
871    set hours [expr {$t / (60 * 60)}]
872    if {$hours == 1} {
873        append string "1 Hour "
874    } elseif {$hours > 1} {
875        append string "$hours Hours "
876    }
877    set t [expr {$t - $hours * (60 * 60)}]
878    set mins [expr {$t / 60}]
879    if {$mins == 1} {
880        append string "1 Minute "
881    } elseif {$mins > 1} {
882        append string "$mins Minutes "
883    }
884    set t [expr {$t - $mins * 60}]
885    if {$t == 1} {
886        append string "1 Second "
887    } elseif {$t > 1} {
888        append string "$t Seconds "
889    }
890    append result "<tr><td>[html::font]<b>Bucket Size</b></font></td>"
891    append result "<td>[html::font]$string</font></td></tr>\n"
892
893    append result "<tr><td>[html::font]<b>Max Per Sec</b></font></td>"
894    append result "<td>[html::font][format %.2f [expr {$max/double($secsForMax)}]]</font></td></tr>\n"
895
896    if {$periodMax > 0} {
897        append result "<tr><td>[html::font]<b>Best Per Sec</b></font></td>"
898        append result "<td>[html::font][format %.2f $periodMax]</font></td></tr>\n"
899    }
900    append result "<tr><td>[html::font]<b>Starting Time</b></font></td>"
901    switch -glob -- $options(-unit) {
902        min* {
903        append result "<td>[html::font][clock format $time \
904            -format %k:%M:%S]</font></td></tr>\n"
905        }
906        hour* {
907        append result "<td>[html::font][clock format $time \
908            -format %k:%M:%S]</font></td></tr>\n"
909        }
910        day* {
911        append result "<td>[html::font][clock format $time \
912            -format "%b %d %k:%M"]</font></td></tr>\n"
913        }
914        default {#ignore}
915    }
916
917    } else {
918
919    # Value-base histogram
920
921    set ix [lsort -integer [array names histogram]]
922
923    set mode [expr {$counter(bucketsize) * $maxName}]
924    set first [expr {$counter(bucketsize) * [lindex $ix 0]}]
925    set last [expr {$counter(bucketsize) * [lindex $ix end]}]
926
927    append result "<tr><td>[html::font]<b>Average</b></font></td>"
928    append result "<td>[html::font][format $options(-format) [counter::get $tag -avg]]</font></td></tr>\n"
929
930    append result "<tr><td>[html::font]<b>Mode</b></font></td>"
931    append result "<td>[html::font]$mode</font></td></tr>\n"
932
933    append result "<tr><td>[html::font]<b>Minimum</b></font></td>"
934    append result "<td>[html::font]$first</font></td></tr>\n"
935
936    append result "<tr><td>[html::font]<b>Maximum</b></font></td>"
937    append result "<td>[html::font]$last</font></td></tr>\n"
938
939    append result "<tr><td>[html::font]<b>Unit</b></font></td>"
940    append result "<td>[html::font]$unit</font></td></tr>\n"
941
942    append result "<tr><td colspan=2 align=center>[html::font]<b>"
943    append result "<a href=[ncgi::urlStub]?resetCounter=$tag>Reset</a></td></tr>\n"
944
945    if {$options(-max) < 0} {
946        set options(-max) [lindex $ix end]
947    }
948    if {![info exists options(-min)]} {
949        set options(-min) [lindex $ix 0]
950    }
951    }
952
953    # End table nested inside left-hand column
954
955    append result </table>\n
956    append result </td>\n
957    append result "<td valign=bottom>\n"
958
959
960    # Display the histogram
961
962    if {$options(-text)} {
963    } else {
964    append result [eval \
965        {counter::histHtmlDisplayBarChart $tag histogram $max $curIndex $time} \
966        [array get options]]
967    }
968
969    # Close the right hand column, but leave our caller's table open.
970
971    append result </td></tr>\n
972
973    return $result
974}
975
976# ::counter::histHtmlDisplayBarChart --
977#
978#   Create an html display of the histogram.
979#
980# Arguments:
981#   tag     The counter tag.
982#   histVar     The name of the histogram array
983#   max     The maximum counter value in a histogram bucket.
984#   curIndex    The "current" histogram index, for time-base histograms.
985#   time        The base, or starting time, for the time-based histograms.
986#   args        The array get of the options passed into histHtmlDisplay
987#
988# Results:
989#   HTML for the bar chart.
990#
991# Side Effects:
992#   See description.
993
994proc ::counter::histHtmlDisplayBarChart {tag histVar max curIndex time args} {
995    upvar #0 counter::T-$tag counter
996    upvar 1 $histVar histogram
997    variable secsPerMinute
998    array set options $args
999
1000    append result "<table cellpadding=0 cellspacing=0 bgcolor=#eeeeee><tr>\n"
1001
1002    set ix [lsort -integer [array names histogram]]
1003
1004    for {set t $options(-min)} {$t < $options(-max)} {incr t} {
1005    if {![info exists histogram($t)]} {
1006        set value 0
1007    } else {
1008        set value $histogram($t)
1009    }
1010    if {$max == 0 || $value == 0} {
1011        set height 1
1012    } else {
1013        set percent [expr {round($value * 100.0 / $max)}]
1014        set height [expr {$percent * $options(-height) / 100}]
1015    }
1016    if {$t == $curIndex} {
1017        set img src=$options(-images)/$options(-ongif)
1018    } else {
1019        set img src=$options(-images)/$options(-gif)
1020    }
1021    append result "<td valign=bottom><img $img height=$height\
1022        width=$options(-width) title=$value alt=$value></td>\n"
1023    }
1024    append result "</tr>"
1025
1026    # Count buckets outside the range requested
1027
1028    set overflow 0
1029    set underflow 0
1030    foreach t [lsort -integer [array names histogram]] {
1031    if {($options(-max) > 0) && ($t > $options(-max))} {
1032        incr overflow
1033    }
1034    if {($options(-min) >= 0) && ($t < $options(-min))} {
1035        incr underflow
1036    }
1037    }
1038
1039    # Append a row of labels at the bottom.
1040
1041    set colors {black #CCCCCC}
1042    set bgcolors {#CCCCCC black}
1043    set colori 0
1044    if {$counter(type) != "-timehist"} {
1045
1046    # Label each bucket with its value
1047    # This is probably wrong for hist2x and hist10x
1048
1049    append result "<tr>"
1050    set skip $options(-skip)
1051    if {![info exists counter(mult)]} {
1052        set counter(mult) 1
1053    }
1054
1055    # These are tick marks
1056
1057    set img src=$options(-images)/$options(-gif)
1058    append result "<tr>"
1059    for {set i $options(-min)} {$i < $options(-max)} {incr i} {
1060        if {(($i % $skip) == 0)} {
1061        append result "<td valign=bottom><img $img height=3 \
1062            width=1></td>\n"
1063        } else {
1064        append result "<td valign=bottom></td>"
1065        }
1066    }
1067    append result </tr>
1068
1069    # These are the labels
1070
1071    append result "<tr>"
1072    for {set i $options(-min)} {$i < $options(-max)} {incr i} {
1073        if {$counter(type) == "-histlog"} {
1074        if {[catch {expr {int(log($i) * $counter(bucketsize))}} x]} {
1075            # Out-of-bounds
1076            break
1077        }
1078        } else {
1079        set x [expr {$i * $counter(bucketsize) * $counter(mult)}]
1080        }
1081        set label [format $options(-format) $x]
1082        if {(($i % $skip) == 0)} {
1083        set color [lindex $colors $colori]
1084        set bg [lindex $bgcolors $colori]
1085        set colori [expr {($colori+1) % 2}]
1086        append result "<td colspan=$skip><font size=1 color=$color>$label</font></td>"
1087        }
1088    }
1089    append result </tr>
1090    } else {
1091    switch -glob -- $options(-unit) {
1092        min*    {
1093        if {$secsPerMinute != 60} {
1094            set format %k:%M:%S
1095            set skip 12
1096        } else {
1097            set format %k:%M
1098            set skip 4
1099        }
1100        set deltaT $secsPerMinute
1101        set wrapDeltaT [expr {$secsPerMinute * -59}]
1102        }
1103        hour*   {
1104        if {$secsPerMinute != 60} {
1105            set format %k:%M
1106            set skip 4
1107        } else {
1108            set format %k
1109            set skip 2
1110        }
1111        set deltaT [expr {$secsPerMinute * 60}]
1112        set wrapDeltaT [expr {$secsPerMinute * 60 * -23}]
1113        }
1114        day* {
1115        if {$secsPerMinute != 60} {
1116            set format "%m/%d %k:%M"
1117            set skip 10
1118        } else {
1119            set format %k
1120            set skip $options(-skip)
1121        }
1122        set deltaT [expr {$secsPerMinute * 60 * 24}]
1123        set wrapDeltaT 0
1124        }
1125        default {#ignore}
1126    }
1127    # These are tick marks
1128
1129    set img src=$options(-images)/$options(-gif)
1130    append result "<tr>"
1131    foreach t [lsort -integer [array names histogram]] {
1132        if {(($t % $skip) == 0)} {
1133        append result "<td valign=bottom><img $img height=3 \
1134            width=1></td>\n"
1135        } else {
1136        append result "<td valign=bottom></td>"
1137        }
1138    }
1139    append result </tr>
1140
1141    set lastLabel ""
1142    append result "<tr>"
1143    foreach t [lsort -integer [array names histogram]] {
1144
1145        # Label each bucket with its time
1146
1147        set label [clock format $time -format $format]
1148        if {(($t % $skip) == 0) && ($label != $lastLabel)} {
1149        set color [lindex $colors $colori]
1150        set bg [lindex $bgcolors $colori]
1151        set colori [expr {($colori+1) % 2}]
1152        append result "<td colspan=$skip><font size=1 color=$color>$label</font></td>"
1153        set lastLabel $label
1154        }
1155        if {$t == $curIndex} {
1156        incr time $wrapDeltaT
1157        } else {
1158        incr time $deltaT
1159        }
1160    }
1161    append result </tr>\n
1162    }
1163    append result "</table>"
1164    if {$underflow > 0} {
1165    append result "<br>Skipped $underflow samples <\
1166        [expr {$options(-min) * $counter(bucketsize)}]\n"
1167    }
1168    if {$overflow > 0} {
1169    append result "<br>Skipped $overflow samples >\
1170        [expr {$options(-max) * $counter(bucketsize)}]\n"
1171    }
1172    return $result
1173}
1174
1175# ::counter::start --
1176#
1177#   Start an interval timer.  This should be pre-declared with
1178#   type either -hist, -hist2x, or -hist20x
1179#
1180# Arguments:
1181#   tag     The counter identifier.
1182#   instance    There may be multiple intervals outstanding
1183#           at any time.  This serves to distinquish them.
1184#
1185# Results:
1186#   None
1187#
1188# Side Effects:
1189#   Records the starting time for the instance of this interval.
1190
1191proc ::counter::start {tag instance} {
1192    upvar #0 counter::Time-$tag time
1193    # clock clicks can return negative values if the sign bit is set
1194    # Here we turn it into a 31-bit counter because we only want
1195    # relative differences
1196    set msec [expr {[clock clicks -milliseconds] & 0x7FFFFFFF}]
1197    set time($instance) [list $msec [clock seconds]]
1198}
1199
1200# ::counter::stop --
1201#
1202#   Record an interval timer.
1203#
1204# Arguments:
1205#   tag     The counter identifier.
1206#   instance    There may be multiple intervals outstanding
1207#           at any time.  This serves to distinquish them.
1208#   func        An optional function used to massage the time
1209#           stamp before putting into the histogram.
1210#
1211# Results:
1212#   None
1213#
1214# Side Effects:
1215#   Computes the current interval and adds it to the histogram.
1216
1217proc ::counter::stop {tag instance {func ::counter::Identity}} {
1218    upvar #0 counter::Time-$tag time
1219
1220    if {![info exists time($instance)]} {
1221	# Extra call. Ignore so we can debug error cases.
1222	return
1223    }
1224    set msec [expr {[clock clicks -milliseconds] & 0x7FFFFFFF}]
1225    set now [list $msec [clock seconds]]
1226    set delMicros [expr {[lindex $now 0] - [lindex $time($instance) 0]}]
1227    if {$delMicros < 0} {
1228      # Microsecond counter wrapped.
1229      set delMicros [expr {0x7FFFFFFF - [lindex $time($instance) 0] +
1230                            [lindex $now 0]}]
1231    }
1232    set delSecond [expr {[lindex $now 1] - [lindex $time($instance) 1]}]
1233    unset time($instance)
1234
1235    # It is quite possible that the millisecond counter is much
1236    # larger than 1000, so we just use it unless our microsecond
1237    # calculation is screwed up.
1238
1239    if {$delMicros >= 0} {
1240      counter::count $tag [$func [expr {$delMicros / 1000.0}]]
1241    } else {
1242      counter::count $tag [$func $delSecond]
1243    }
1244}
1245
1246# ::counter::Identity --
1247#
1248#   Return its argument.  This is used as the default function
1249#   to apply to an interval timer.
1250#
1251# Arguments:
1252#   x       Some value.
1253#
1254# Results:
1255#   $x
1256#
1257# Side Effects:
1258#   None
1259
1260
1261proc ::counter::Identity {x} {
1262    return $x
1263}
1264
1265package provide counter 2.0.4
1266