Watcher.mesa
Copyright (C) 1985, 1986 Xerox Corporation. All rights reserved.
Hal Murray, March 25, 1986 9:05:46 pm PST
Russ Atkinson (RRA) February 25, 1986 11:42:12 am PST
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: BOOLFALSE,
watcher: PROCESSNIL
];
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];
Max bounding box of displayed numbers
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]];
Imager.MaskRectangle[context, [x: -left, y: 0, w: left+data.width+left, h: 1]];
};
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]];
Imager.MaskRectangle[context, [x: -left, y: 0, w: left+data.width+left, h: 1]];
};
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 {
There is something worth plotting. Make sure that it gets at least one pixel.
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;
This forces us to paint fully, since we have wrapped around and are now losing plots.
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] = {
n IN [0..1.0] => 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 {
Come up with an approximation of the maximum idle rate. This must change if Watch.IdleProcess changes!
refInt: REF INTNEW[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;
{
Adaptively calculate the CPU rate
idleRate ← (idleCount-lastIdleCount)/sec;
maxIdleRate ← MAX[maxIdleRate, idleRate];
cpu ← 100*(1.0 - idleRate/maxIdleRate);
};
{
Allocator
temp: INT ← SafeStorage.NWordsAllocated[];
words: INT ← temp-oldAlloc;
oldAlloc ← temp;
alloc ← REAL[words]/sec;
};
{
Disk
temp: INT ← Disk.GetStatistics[].readPages + Disk.GetStatistics[].writePages;
pages: INT ← temp-oldDisk;
oldDisk ← temp;
disk ← REAL[16*256*pages]/sec;
};
{
Ethernet
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 {
We must paint in the Frames and stuff
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 {
The window width has changed, so refigure the width
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"];
}.