<> <> <> <> DIRECTORY BasicTime USING [GetClockPulses, Pulses, PulsesToSeconds], Commander USING [CommandProc, Register], Containers USING [Container, Create], Convert USING [AppendF, AppendInt, AppendRope, RopeFromInt], CedarProcess USING [SetPriority], Disk USING [GetStatistics], EthernetDriverStats USING [EtherStats, EtherStatsRep, GetEthernetOneStats, GetEthernetStats], Imager USING [black, Context, DoSave, MaskRectangle, SetColor, SetFont, SetXY, SetXYI, ShowRope, ShowText, TranslateT, white], ImagerFont USING [Extents, Find, Font, RopeWidth, TextBoundingBox], PrincOpsUtils USING [ReadWDC], Process USING [MsecToTicks, Pause], Real USING [Round, RoundI], RealFns USING [Log], RefText USING [ObtainScratch, ReleaseScratch], Rope USING [ROPE], SafeStorage USING [NWordsAllocated], ViewerClasses USING [PaintProc, Viewer, ViewerClass, ViewerClassRec], ViewerEvents USING [EventProc, RegisterEventProc], ViewerForkers USING [ForkPaint], ViewerOps USING [CreateViewer, OpenIcon, RegisterViewerClass, SetOpenHeight], WatchStats USING [GetWatchStats]; Watcher: CEDAR MONITOR LOCKS data USING data: Data IMPORTS BasicTime, CedarProcess, Commander, Containers, Convert, Disk, EthernetDriverStats, Imager, ImagerFont, PrincOpsUtils, Process, Real, RealFns, RefText, SafeStorage, ViewerEvents, ViewerForkers, ViewerOps, WatchStats = { Context: TYPE ~ Imager.Context; Font: TYPE ~ ImagerFont.Font; ROPE: TYPE ~ Rope.ROPE; Data: TYPE = REF DataRecord; DataRecord: TYPE = MONITORED RECORD [ container: Containers.Container _ NIL, graphs: ViewerClasses.Viewer _ NIL, cpu: REF ArrayOfPointsRep _ NEW [ArrayOfPointsRep _ ALL[0]], cpuSample, cpuAvg: REAL _ 0.0, alloc: REF ArrayOfPointsRep _ NEW [ArrayOfPointsRep _ ALL[0]], allocSample, allocAvg: REAL _ 0.0, disk: REF ArrayOfPointsRep _ NEW [ArrayOfPointsRep _ ALL[0]], diskSample, diskAvg: REAL _ 0.0, ether: REF ArrayOfPointsRep _ NEW [ArrayOfPointsRep _ ALL[0]], etherSample, etherAvg: REAL _ 0.0, helvetica8: Font _ ImagerFont.Find["Xerox/TiogaFonts/Helvetica8"], helvetica10: Font _ ImagerFont.Find["Xerox/TiogaFonts/Helvetica10"], lastww: NAT _ 0, nextAdd: NAT _ 0, nextPlot: NAT _ 0, width: NAT _ maxWidth, lostPlots: BOOL _ FALSE, watcher: PROCESS _ NIL ]; leading: NAT = 2; separation: NAT = 10; buttonHeight: NAT = 15; headerHeight: NAT = 12; left: NAT = 72; right: NAT = 40; bottom: NAT = leading+headerHeight+leading; top: NAT = 10; height: NAT = 50; numberX: NAT _ 0; numberY: NAT _ 40; numberW: NAT _ 32; numberH: NAT _ 16; numberExt: ImagerFont.Extents _ [0, 0, 0, 0]; <> titleX: INTEGER _ 4; titleY: INTEGER _ 10; subTitleX: INTEGER _ 10; subTitleY: INTEGER _ -2; averageFactor: REAL _ 0.9; maxWidth: NAT = 60*8; -- It is nice to keep this an integral # of minutes totalHeight: NAT = 4*(top+height+bottom); logScale: NAT _ 6; -- pixels per factor of 2 ArrayOfPoints: TYPE = REF ArrayOfPointsRep; ArrayOfPointsRep: TYPE = ARRAY [0..maxWidth) OF REAL; Title: PROC [context: Context, data: Data, t1, t2: ROPE] ~ { Imager.SetFont[context, data.helvetica10]; Imager.SetXYI[context, -left+titleX, titleY]; Imager.ShowRope[context, t1]; Imager.SetFont[context, data.helvetica8]; Imager.SetXYI[context, -left+subTitleX, subTitleY]; Imager.ShowRope[context, t2]; }; BasicFrame: PROC [context: Context, data: Data, height: NAT] ~ { Imager.SetFont[context, data.helvetica8]; Imager.SetColor[context, Imager.white]; Imager.MaskRectangle[context, [x: 0, y: 0, w: data.width, h: height]]; Imager.SetColor[context, Imager.black]; Imager.MaskRectangle[context, [x: -1, y: 0, w: data.width+2, h: -2.0]]; Imager.MaskRectangle[context, [x: 0, y: -1, w: -1.0, h: height+1]]; Imager.MaskRectangle[context, [x: data.width, y: -1, w: 1.0, h: height+1]]; FOR i: INT _ 0, i + 60 UNTIL i > data.width DO Imager.MaskRectangle[context, [x: i, y: -1, w: -1.0, h: -5.0]]; ENDLOOP; }; Frame: PROC [context: Context, data: Data, max: INT] ~ { delta: NAT = 2; BasicFrame[context, data, height]; FOR i: INT _ 0, i + 25 UNTIL i > height DO value: INT = max*i/height; tag: ROPE = Convert.RopeFromInt[value]; ropeWidth: REAL = ImagerFont.RopeWidth[data.helvetica8, tag].x; Imager.SetXY[context, [0-ropeWidth-delta*2, i]]; IF i # 0 THEN Imager.ShowRope[context, tag]; Imager.MaskRectangle[context, [x: -1, y: i, w: -delta, h: -1.0]]; Imager.MaskRectangle[context, [x: data.width+1, y: i, w: +delta, h: -1.0]]; Imager.SetXYI[context, 0+data.width+delta*2, i]; Imager.ShowRope[context, tag]; ENDLOOP; }; FrameLog: PROC [context: Context, data: Data, max: INT] ~ { tickSize: NAT = 30; value: INT _ max; delta: NAT = 2; BasicFrame[context, data, height]; FOR i: INT _ height, i - tickSize UNTIL i < 0 DO tag: ROPE = Convert.RopeFromInt[value]; ropeWidth: REAL = ImagerFont.RopeWidth[data.helvetica8, tag].x; Imager.SetXY[context, [0-ropeWidth-delta*2, i]]; IF i # 0 THEN Imager.ShowRope[context, tag]; Imager.MaskRectangle[context, [x: -1, y: i, w: -delta, h: -1.0]]; Imager.MaskRectangle[context, [x: data.width+1, y: i, w: +delta, h: -1.0]]; Imager.SetXYI[context, 0+data.width+delta*2, i]; Imager.ShowRope[context, tag]; FOR i: NAT IN [0..tickSize/logScale) DO value _ value/2; ENDLOOP; ENDLOOP; }; InitData: PROC [context: Context, data: Data, max: INT, t1, t2: ROPE] = { scale: INT = max/height; Imager.TranslateT[context, [0, -(height + top)]]; Frame[context, data, max]; Title[context, data, t1, t2]; Imager.TranslateT[context, [0, -bottom]]; <> }; InitLog: PROC [context: Context, data: Data, max: INT, t1, t2: ROPE] = { Imager.TranslateT[context, [0, -(height + top)]]; FrameLog[context, data, max]; Title[context, data, t1, t2]; Imager.TranslateT[context, [0, -bottom]]; <> }; PlotThickens: PROC [context: Context, data: Data, sample: REAL, i: NAT, base: NAT, update: BOOL] = { IF update THEN { quarter: NAT _ height/4; half: NAT _ quarter+quarter; next: NAT _ (i + 10) MOD data.width; Imager.SetColor[context, Imager.white]; Imager.MaskRectangle[context, [x: next, y: base, w: 1.0, h: height]]; Imager.MaskRectangle[context, [x: i, y: base, w: 1.0, h: height]]; Imager.SetColor[context, Imager.black]; Imager.MaskRectangle[context, [x: i+1, y: base+quarter-2, w: 1.0, h: 4]]; -- tick marks Imager.MaskRectangle[context, [x: i+1, y: base+half-2, w: 1.0, h: 4]]; Imager.MaskRectangle[context, [x: i+1, y: base+quarter+half-2, w: 1.0, h: 4]]; }; IF sample > 0 THEN { <> h: NAT _ Real.Round[sample]; Imager.MaskRectangle[context, [x: i, y: base, w: 1.0, h: MAX[h, 1]]]; }; }; ShowNumber: PROC [context: Context, data: Data, sample: REAL, sampleAvg: REAL, base: NAT, point: BOOL] = { text: REF TEXT _ RefText.ObtainScratch[16]; x: INTEGER _ numberX-left+numberW; y: INTEGER _ base+numberY; SELECT TRUE FROM sample = 0 => text _ Convert.AppendRope[text, "0", FALSE]; point AND sample < 0.05 => text _ Convert.AppendRope[text, "< 0.1", FALSE]; point AND sample < 9.95 => text _ Convert.AppendF[text, sample, 1]; sample < 0.5 => text _ Convert.AppendRope[text, "< 1", FALSE]; ENDCASE => text _ Convert.AppendInt[text, Real.Round[sample]]; ShowNumberText[context, data.helvetica8, x, y, text]; text.length _ 0; SELECT TRUE FROM sampleAvg < 0.005 => text _ Convert.AppendRope[text, "0", FALSE]; point AND sampleAvg < 0.05 => text _ Convert.AppendRope[text, "< 0.1", FALSE]; point AND sampleAvg < 9.95 => text _ Convert.AppendF[text, sampleAvg, 1]; sampleAvg < 0.5 => text _ Convert.AppendRope[text, "< 1", FALSE]; ENDCASE => text _ Convert.AppendInt[text, Real.Round[sampleAvg]]; ShowNumberText[context, data.helvetica8, x, y-numberH, text]; RefText.ReleaseScratch[text]; }; ShowNumberText: PROC [context: Context, font: ImagerFont.Font, x, y: INTEGER, text: REF TEXT] = { ext: ImagerFont.Extents _ ImagerFont.TextBoundingBox[font, text]; w: INTEGER; r: INTEGER; h: INTEGER; d: INTEGER; IF ext.leftExtent > numberExt.leftExtent THEN numberExt.leftExtent _ ext.leftExtent; IF ext.rightExtent > numberExt.rightExtent THEN numberExt.rightExtent _ ext.rightExtent; IF ext.descent > numberExt.descent THEN numberExt.descent _ ext.descent; IF ext.ascent > numberExt.ascent THEN numberExt.ascent _ ext.ascent; r _ Real.RoundI[numberExt.rightExtent]; w _ r-Real.RoundI[numberExt.leftExtent]; d _ Real.RoundI[numberExt.descent]; h _ d+Real.RoundI[numberExt.ascent]; Imager.SetColor[context, Imager.white]; Imager.MaskRectangle[context, [x: x-w, y: y-d, w: w, h: h]]; Imager.SetColor[context, Imager.black]; Imager.SetXYI[context, x-Real.RoundI[ext.rightExtent], y]; Imager.SetFont[context, font]; Imager.ShowText[context, text]; }; MaxExtent: PROC [max, new: ImagerFont.Extents] RETURNS [ImagerFont.Extents] = { IF new.leftExtent < max.leftExtent THEN max.leftExtent _ new.leftExtent; IF new.rightExtent > max.rightExtent THEN max.rightExtent _ new.rightExtent; IF new.descent > max.descent THEN max.descent _ new.descent; IF new.ascent > max.ascent THEN max.ascent _ new.ascent; RETURN [max]; }; ResetPlotted: ENTRY PROC [data: Data, newWidth: NAT] = { nextPlot: NAT _ data.nextAdd+10; IF nextPlot >= data.width THEN nextPlot _ nextPlot-data.width; data.nextPlot _ nextPlot; data.lostPlots _ FALSE; IF newWidth # data.width THEN { data.nextAdd _ data.nextPlot _ 0; data.width _ newWidth; data.cpu^ _ ALL[0.0]; data.alloc^ _ ALL[0.0]; data.disk^ _ ALL[0.0]; data.ether^ _ ALL[0.0]; }; }; GetSample: ENTRY PROC [data: Data] RETURNS [index: INTEGER _ -1] = { nextAdd: NAT _ data.nextAdd; nextPlot: NAT _ data.nextPlot; IF data.lostPlots OR nextAdd # nextPlot THEN { avgComp: REAL _ 1.0-averageFactor; next: NAT _ (index _ nextPlot)+1; IF next = data.width THEN next _ 0; data.nextPlot _ next; data.cpuAvg _ data.cpuAvg*averageFactor + (data.cpuSample _ data.cpu[index])*avgComp; data.allocAvg _ data.allocAvg*averageFactor + (data.allocSample _ data.alloc[index])*avgComp; data.diskAvg _ data.diskAvg*averageFactor + (data.diskSample _ data.disk[index])*avgComp; data.etherAvg _ data.etherAvg*averageFactor + (data.etherSample _ data.ether[index])*avgComp; data.lostPlots _ FALSE; }; }; AddSample: ENTRY PROC [data: Data, cpu, alloc, disk, ether: REAL] = { index: NAT _ data.nextAdd; next: NAT _ index+1; IF next = data.width THEN next _ 0; IF next = data.nextPlot THEN data.lostPlots _ TRUE; <> data.nextAdd _ next; data.cpu[index] _ cpu; data.alloc[index] _ alloc; data.disk[index] _ disk; data.ether[index] _ ether; }; Log: PROC [n: REAL] RETURNS [log: REAL] = { < log IN [0..height]>> IF n <= 0 THEN RETURN[0]; log _ height + logScale*RealFns.Log[2, n]; IF log < 0 THEN log _ 0; }; AntiLog: PROC [ln: INT] RETURNS [n: INT _ 1] = { UNTIL ln = 0 DO ln _ ln - 1; n _ n+n; ENDLOOP; n _ n/2; }; milliSecondsPerPixel: INT _ 1000; Collector: PROC [data: Data] = { ENABLE ABORTED => CONTINUE; viewer: ViewerClasses.Viewer _ data.container; start, stop: BasicTime.Pulses; sec: REAL; i: NAT _ 0; idleCount: INT _ WatchStats.GetWatchStats[].idleCount; maxIdleRate: REAL _ 10000; -- Cedar will not really run on slower machines anyway! idleRate: REAL _ 0; oldAlloc: INT _ SafeStorage.NWordsAllocated[]; oldDisk: INT _ Disk.GetStatistics[].readPages + Disk.GetStatistics[].writePages; etherStats: EthernetDriverStats.EtherStats _ EthernetDriverStats.GetEthernetOneStats[0]; oldEther: EthernetDriverStats.EtherStatsRep; IF etherStats = NIL THEN etherStats _ EthernetDriverStats.GetEthernetStats[0]; oldEther _ etherStats^; CedarProcess.SetPriority[excited]; TRUSTED { <> refInt: REF INT _ NEW[INT _ 0]; stop _ BasicTime.GetClockPulses[]; THROUGH [0..10000) DO refInt^ _ refInt^ + 1; IF PrincOpsUtils.ReadWDC[] # 0 THEN ERROR; ENDLOOP; start _ BasicTime.GetClockPulses[]; maxIdleRate _ 10000/BasicTime.PulsesToSeconds[start-stop]; }; DO lastIdleCount: INT _ idleCount; cpu, alloc, disk, ether: REAL _ 0.0; Process.Pause[Process.MsecToTicks[milliSecondsPerPixel]]; stop _ BasicTime.GetClockPulses[]; sec _ BasicTime.PulsesToSeconds[stop-start]; idleCount _ WatchStats.GetWatchStats[].idleCount; start _ stop; IF sec = 0 THEN LOOP; { <> idleRate _ (idleCount-lastIdleCount)/sec; maxIdleRate _ MAX[maxIdleRate, idleRate]; cpu _ 100*(1.0 - idleRate/maxIdleRate); }; { <> temp: INT _ SafeStorage.NWordsAllocated[]; words: INT _ temp-oldAlloc; oldAlloc _ temp; alloc _ REAL[words]/sec; }; { <> temp: INT _ Disk.GetStatistics[].readPages + Disk.GetStatistics[].writePages; pages: INT _ temp-oldDisk; oldDisk _ temp; disk _ REAL[16*256*pages]/sec; }; { <> temp: EthernetDriverStats.EtherStatsRep _ etherStats^; words: INT _ (temp.wordsRecv-oldEther.wordsRecv) + (temp.wordsSent-oldEther.wordsSent); oldEther _ temp; ether _ REAL[16*words]/sec; }; AddSample[data, cpu, alloc, disk, ether]; IF viewer.destroyed THEN RETURN; IF NOT viewer.iconic THEN ViewerForkers.ForkPaint[viewer, client, FALSE, $Update, TRUE]; ENDLOOP; }; RepaintViewer: ViewerClasses.PaintProc = { <<[self: ViewerClasses.Viewer, context: Imager.Context, whatChanged: REF ANY, clear: BOOL] RETURNS [quit: BOOL _ FALSE]>> data: Data = NARROW[self.data]; ww: NAT _ data.container.ww; update: BOOL _ whatChanged = $Update AND NOT data.lostPlots AND ww = data.lastww; cpuAvg, allocAvg, diskAvg, etherAvg: REAL _ 0.0; Imager.TranslateT[context, [left, bottom]]; IF NOT update THEN { <> newWidth: INTEGER _ data.width; init: PROC = { Imager.TranslateT[context, [0, totalHeight-bottom]]; InitData[context, data, 100, "Cpu", "% busy"]; InitLog[context, data, 64, "Alloc", "Kwds/s"]; InitLog[context, data, 3200, "Disk", "Kbits/s"]; InitLog[context, data, 3200, "Ether", "Kbits/s"]; }; IF ww # data.lastww THEN { <> newWidth _ MAX[60, MIN[maxWidth, ww - left - right]]; IF newWidth < 60 THEN newWidth _ 60; data.lastww _ ww; }; ResetPlotted[data, newWidth]; Imager.DoSave[context, init]; }; Imager.SetColor[context, Imager.black]; DO sample: REAL _ 0.0; base: NAT _ 0; index: INTEGER _ GetSample[data]; IF index < 0 THEN RETURN; sample _ data.etherSample; PlotThickens[context, data, Log[MIN[sample/3200000, 1.0]], index, base, update]; IF update THEN ShowNumber[context, data, sample*1e-3, data.etherAvg*1e-3, base, TRUE]; base _ base + (bottom+height+top); sample _ data.diskSample; PlotThickens[context, data, Log[MIN[sample/3200000, 1.0]], index, base, update]; IF update THEN ShowNumber[context, data, sample*1e-3, data.diskAvg*1e-3, base, TRUE]; base _ base + (bottom+height+top); sample _ data.allocSample; PlotThickens[context, data, Log[MIN[sample/64000, 1.0]], index, base, update]; IF update THEN ShowNumber[context, data, sample*1e-3, data.allocAvg*1e-3, base, TRUE]; base _ base + (bottom+height+top); sample _ data.cpuSample; PlotThickens[context, data, MIN[sample/(100/height), REAL[height]], index, base, update]; IF update THEN ShowNumber[context, data, sample, data.cpuAvg, base, FALSE]; ENDLOOP; }; global: Data _ NIL; Poof: ViewerEvents.EventProc = { data: Data = NARROW[viewer.data]; IF global = data THEN global _ NIL; }; MakeTool: Commander.CommandProc = { data: Data _ NEW [DataRecord]; data.container _ Containers.Create[ info: [ name: "Watcher", iconic: TRUE, column: left, menu: NIL, scrollable: FALSE, data: data ], paint: FALSE]; ViewerOps.SetOpenHeight[data.container, totalHeight]; data.graphs _ ViewerOps.CreateViewer[ flavor: $Watcher, info: [ parent: data.container, wx: 0, wy: 0, ww: left+maxWidth+left, wh: totalHeight, scrollable: FALSE, border: FALSE, data: data], paint: FALSE]; [] _ ViewerEvents.RegisterEventProc[Poof, destroy, data.graphs, TRUE]; global _ data; data.watcher _ FORK Collector[data]; ViewerOps.OpenIcon[icon: data.container]; }; graphClass: ViewerClasses.ViewerClass _ NEW[ViewerClasses.ViewerClassRec _ [paint: RepaintViewer]]; ViewerOps.RegisterViewerClass[$Watcher, graphClass]; Commander.Register["Watcher", MakeTool, "Make Tool for watching performance numbers"]; }.