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] = { 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 = { 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"]; }. Ά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 Max bounding box of displayed numbers Imager.MaskRectangle[context, [x: -left, y: 0, w: left+data.width+left, h: 1]]; Imager.MaskRectangle[context, [x: -left, y: 0, w: left+data.width+left, h: 1]]; There is something worth plotting. Make sure that it gets at least one pixel. This forces us to paint fully, since we have wrapped around and are now losing plots. n IN [0..1.0] => log IN [0..height] Come up with an approximation of the maximum idle rate. This must change if Watch.IdleProcess changes! Adaptively calculate the CPU rate Allocator Disk Ethernet [self: ViewerClasses.Viewer, context: Imager.Context, whatChanged: REF ANY, clear: BOOL] RETURNS [quit: BOOL _ FALSE] We must paint in the Frames and stuff The window width has changed, so refigure the width Κή˜codešœ ™ K™AK™)K™5K˜šΟk ˜ Kšœ œ+˜:Kšœ œ˜(Kšœ œ˜%Kšœœ/˜˜MKšœ œ˜!——headšœ œ˜Kšœœ ˜šœά˜γK˜—Kšœ œ˜Kšœœ˜Kšœœœ˜K˜Kšœœœ ˜šœ œœ˜%Kšœ"œ˜&Kšœœ˜#Kšœœœœ˜Kšœœ˜"Kšœœœœ˜=Kšœœ˜ Kšœœœœ˜>Kšœœ˜"KšœB˜BKšœD˜DKšœœ˜Kšœ œ˜Kšœ œ˜Kšœœ ˜Kšœ œœ˜Kšœ œ˜Kšœ˜K˜—Kšœ œ˜Kšœ œ˜Kšœœ˜Kšœœ˜Kšœœ˜Kšœœ˜Kšœœ ˜+Kšœœ˜Kšœœ˜K˜Kšœ œ˜Kšœ œ˜Kšœ œ˜Kšœ œ˜šœ-˜-Kšœ%™%—K˜Kšœœ˜Kšœœ˜Kšœ œ˜Kšœ œ˜K˜Kšœœ˜K˜Kšœ œ Οc3˜IK˜Kšœ œ˜)K˜Kšœ œž˜,K˜Kšœœœ˜+Kš œœœœœ˜5K˜šΟnœœ(œ˜Kšœ7˜>—Kšœ5˜5K˜šœœ˜Kšœ:œ˜AKšœœ>œ˜NKšœœ@˜IKšœ:œ˜AKšœ:˜A—Kšœ=˜=Kšœ˜K˜K˜—š Ÿœœ1œœœ˜aKšœA˜AKšœœ˜ Kšœœ˜ Kšœœ˜ Kšœœ˜ Kšœ'œ'˜TKšœ)œ)˜XKšœ!œ!˜HKšœœ˜DKšœ'˜'Kšœ(˜(Kšœ#˜#Kšœ$˜$Kšœ'˜'Kšœ<˜Kšœ˜Kšœœ˜šœœ˜Kšœ!˜!Kšœ˜Kšœ œ˜Kšœœ˜Kšœ œ˜Kšœœ˜K˜—K˜K˜—š Ÿ œœœœ œ ˜DKšœ œ˜Kšœ œ˜šœœœ˜.Kšœ œ˜"Kšœœ˜!K˜Kšœœ ˜#Kšœ˜KšœU˜UKšœ]˜]KšœY˜YKšœ]˜]Kšœœ˜K˜—K˜K˜—šŸ œœœ'œ˜EKšœœ˜Kšœœ ˜Kšœœ ˜#šœœœ˜3KšœU™U—Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜K˜K˜—š Ÿœœœœœ˜+Kšœœœ ™#Kšœœœ˜Kšœ*˜*Kšœ œ ˜K˜K˜—š Ÿœœœœœ ˜0Kšœœœ˜.K˜K˜K˜—Kšœœ˜!šŸ œœ˜ Kšœœœ˜K˜.K˜Kšœœ˜ Kšœœ˜ Kšœ œ(˜6Kšœ œ ž7˜SKšœ œ˜Kšœ œ!˜.Kšœ œD˜PKšœX˜XKšœ,˜,Kšœœœ6˜NKšœ˜Kšœ"˜"K˜šœ˜ Kšœg™gKš œœœœœ˜K˜"šœ ˜Kšœ˜Kšœœœ˜*Kšœ˜—K˜#Kšœ:˜:K˜K˜—š˜Kšœœ ˜Kšœœ˜$Kšœ9˜9K˜"Kšœ,˜,Kšœ1˜1K˜ Kšœ œœ˜˜Kšœœ™!Kšœ)˜)Kšœœ˜)Kšœ'˜'K˜—šœ˜Kšœ ™ Kšœœ!˜*Kšœœ˜Kšœ˜Kšœœ ˜Kšœ˜—šœ˜Kšœ™KšœœD˜MKšœœ˜Kšœ˜Kšœœ˜Kšœ˜—šœ˜Kšœ™Kšœ6˜6KšœœM˜WK˜Kšœœ˜Kšœ˜—Kšœ)˜)Kšœœœ˜ šœœ˜Kšœ(œ œ˜>—Kšœ˜—Kšœ˜K˜—•StartOfExpansiony -- [self: ViewerClasses.Viewer, context: Imager.Context, whatChanged: REF ANY, clear: BOOL] RETURNS [quit: BOOL _ FALSE]šœ*˜*KšΠcku™uKšœ œ ˜Kšœœ˜Kš œœœœœ˜QKšœ%œ˜0Kšœ+˜+šœœœ˜Kšœ%™%Kšœ œ˜šœœ˜Kšœ4˜4Kšœ.˜.Kšœ.˜.Kšœ0˜0Kšœ1˜1K˜—šœœ˜Kšœ3™3Kšœ œœ˜5Kšœœ˜$Kšœ˜K˜—Kšœ˜Kšœ˜Kšœ˜—Kšœ'˜'š˜Kšœœ˜Kšœœ˜Kšœœ˜!Kšœ œœ˜K˜Kšœ˜Kšœ œ-˜PKšœœBœ˜VKšœ"˜"K˜Kšœ˜Kšœ œ-˜PKšœœAœ˜UKšœ"˜"K˜Kšœ˜Kšœ œ+˜NKšœœBœ˜VKšœ"˜"K˜Kšœ˜Kšœœœ ˜YKšœœ6œ˜KK˜Kšœ˜—Kšœ˜K˜—Kšœœ˜šΟbœ˜ Kšœ œ˜!Kšœœ œ˜#Kšœ˜K˜—š‘œ˜#Kšœ œ˜šœ#˜#šœ˜Kšœ˜Kšœœ˜ Kšœ ˜ Kšœœ˜ Kšœ œ˜Kšœ ˜ —Kšœœ˜—Kšœ5˜5šœ%˜%Kšœ˜šœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœ œ˜Kšœœ˜Kšœ ˜ —Kšœœ˜—Kšœ@œ˜FK˜Kšœœ˜$Kšœ)˜)Kšœ˜K˜—šœ(œ˜JKšœ˜K˜—Kšœ4˜4KšœV˜VK˜Kšœ˜——…—