DIRECTORY BasicTime USING [GetClockPulses, Now, Pulses, PulsesToMicroseconds, PulsesToSeconds, SetTime, Unpack, Unpacked], Booting USING [CallBootingProcs], Buttons USING [Button, ButtonProc, Create, SetDisplayStyle], CedarProcess USING [SetPriority], Containers USING [ChildXBound, Create], DebuggerSwap USING [WorryCallDebugger], DefaultRemoteNames USING [Get], Disk USING [GetStatistics], EthernetDriverStats USING [EtherStats, EtherStatsRep, GetEthernetOneStats, GetEthernetStats], File USING [SystemVolume], FileBackdoor USING [GetVolumePages], FileStats USING [Data, GetData], FS USING [Error, ExpandName], FSBackdoor USING [NextRemoteEvent, RemoteEvent, RemoteOp], Imager USING [black, Color, MakeGray, MaskBox, SetColor, white], Labels USING [Create, Label, Set, SetDisplayStyle], Idle USING [IsIdle], Icons USING [IconFlavor, NewIconFromFile], IO USING [PutFR], Menus USING [MenuProc], NumberLabels USING [CreateNumber, NumberLabel, NumberLabelUpdate], PrincOps USING [GFT, GFTItem, PageCount, SD, sGFTLength], PrincOpsUtils USING [ReadWDC, WriteWDC], Process USING [Detach, MsecToTicks, Pause, SecondsToTicks, SetTimeout], ProcessorFace USING [PowerOff], Real USING [Round, RoundI], Rope USING [Concat, Find, Length, ROPE, Substr], SafeStorage USING [NWordsAllocated, ReclaimCollectibleObjects, ReclamationReason, SetCollectionInterval, WaitForCollectorDone, WaitForCollectorStart], UserProfile USING [Boolean, CallWhenProfileChanges, Number, ProfileChangedProc], VFonts USING [StringWidth], ViewerClasses USING [DestroyProc, PaintProc, Viewer, ViewerClass, ViewerClassRec], ViewerOps USING [ComputeColumn, CreateViewer, PaintViewer, RegisterViewerClass, SetOpenHeight, ViewerColumn], ViewerSpecs USING [openRightWidth], VM USING [Allocate, CantAllocate, PageCount], VMStatistics USING [checkoutConflicts, laundryCleanCalls, laundryWakeups, pageFaults, pagesCleaned, pagesCleanedPanic, panicLaundryWakeups, pinnedPages, readOnlyPages, rmAllocPasses, rmCleanPasses, rmDirty, rmFreeList, rmNewClean, rmOldClean, rmReclamations, swapInAlreadyIn, swapInCalls, swapInDirtyVictims, swapInFailedToCleanVictims, swapInNoRead, swapInPages, swapInPhysicalRuns, swapInReads, swapInVirtualRuns, uselessLaundryWakeups, VirtualAllocation], WatchStats USING [WatchStatsRecord]; Watch: CEDAR MONITOR IMPORTS BasicTime, Booting, Buttons, CedarProcess, Containers, DebuggerSwap, DefaultRemoteNames, Disk, EthernetDriverStats, File, FileBackdoor, FileStats, FS, FSBackdoor, Imager, Labels, Idle, Icons, IO, NumberLabels, PrincOpsUtils, Process, ProcessorFace, Real, Rope, SafeStorage, UserProfile, VFonts, ViewerOps, ViewerSpecs, VM, VMStatistics EXPORTS WatchStats = BEGIN Button: TYPE = Buttons.Button; Label: TYPE = Labels.Label; NumberLabel: TYPE = NumberLabels.NumberLabel; RemoteEventHandle: TYPE = REF READONLY FSBackdoor.RemoteEvent; RemoteOp: TYPE = FSBackdoor.RemoteOp; ROPE: TYPE = Rope.ROPE; GraphData: TYPE = REF GraphDataRec; GraphEntry: TYPE = RECORD [ value: REAL _ 0, -- current value (scaled) updateHint: UpdateHint _ bigger, fullScale: REAL]; -- log of full scale value (-1 = linear in [0..1]) UpdateHint: TYPE = {bigger, smaller, same}; GraphDataRec: TYPE = RECORD [top, middle, bottom: GraphEntry]; Parameter: TYPE = REF ParameterBlock; ParameterBlock: TYPE = RECORD [ action: PROC [Parameter], label: Label, value,increment,lo,hi: INT]; IconArray: TYPE = REF IconArrayRep; IconArrayRep: TYPE = ARRAY [0..7] OF Icons.IconFlavor; watchStats: WatchStats.WatchStatsRecord; GetWatchStats: PUBLIC ENTRY PROC RETURNS [WatchStats.WatchStatsRecord] = { RETURN [watchStats]; }; quit: BOOLEAN _ FALSE; -- watched by various processes for exit notification waitCond: CONDITION; fsPause: CONDITION _ [timeout: Process.MsecToTicks[200]]; newOpenHeight: INTEGER _ 0; oldOpenHeight: INTEGER _ 0; smallerOpenHeight: INTEGER _ 0; middleOpenHeight: INTEGER _ 0; biggerOpenHeight: INTEGER _ 0; defaultInterval: INT _ 16000; defaultSample: INT _ 2; longPause: INT _ 30 * 1000; millisSinceLastBigBang: INT _ longPause; powerOffAfter: INT _ 1900; powerOffBefore: INT _ 700; idleMinutesTilPowerOff: INT _ 10; minutesSpentIdle: INT _ 0; millisSpentIdle: INT _ 0; minutesSinceTimeSet: INT _ 0; idleCpuRate: REAL _ 0.05; -- all samples must remain below this rate decayCpuRate: REAL _ 0.0; iconArray: IconArray _ NIL; myGrey: Imager.Color ~ Imager.MakeGray[0.5]; GraphPaint: ViewerClasses.PaintProc = { data: GraphData = NARROW[self.data]; IF whatChanged = NIL THEN data.bottom.updateHint _ data.middle.updateHint _ data.top.updateHint _ bigger; SELECT data.top.updateHint FROM bigger => { Imager.SetColor[context, myGrey]; Imager.MaskBox[context, [0, 2*self.ch/3, data.top.value, self.ch]]; }; smaller => { Imager.SetColor[context, Imager.white]; Imager.MaskBox[context, [data.top.value, 2*self.ch/3, self.cw, self.ch]]; }; ENDCASE; SELECT data.middle.updateHint FROM bigger => { Imager.SetColor[context, myGrey]; Imager.MaskBox[context, [0, self.ch/3, data.middle.value, 2*self.ch/3]]; }; smaller => { Imager.SetColor[context, Imager.white]; Imager.MaskBox[context, [data.middle.value, self.ch/3, self.cw, 2*self.ch/3]]; }; ENDCASE; SELECT data.bottom.updateHint FROM bigger => { Imager.SetColor[context, myGrey]; Imager.MaskBox[context, [0, 0, data.bottom.value, self.ch/3]]; }; smaller => { Imager.SetColor[context, Imager.white]; Imager.MaskBox[context, [data.bottom.value, 0, self.cw, self.ch/3]]; }; ENDCASE; Imager.SetColor[context, Imager.black] }; Log10: PROC [x: REAL] RETURNS [lx: REAL] = { sqrt10: REAL = 3.162278; t: REAL; SELECT x FROM < 1e0 => {x _ 1.0; lx _ 0}; < 1e1 => lx _ 0; < 1e2 => {x _ x * 1e-1; lx _ 1}; < 1e3 => {x _ x * 1e-2; lx _ 2}; < 1e4 => {x _ x * 1e-3; lx _ 3}; < 1e5 => {x _ x * 1e-4; lx _ 4}; < 1e6 => {x _ x * 1e-5; lx _ 5}; ENDCASE => {x _ 1.0; lx _ 6}; IF x > sqrt10 THEN {x _ x*(1/sqrt10); lx _ lx + 0.5}; t _ (x - 1)/(x + 1); lx _ lx + (0.86304 + 0.36415*(t*t))*t; }; GraphSet: PROC [graph: ViewerClasses.Viewer, t, m, b: REAL] = { tempR: REAL; myData: GraphData _ NARROW[graph.data]; IF myData = NIL THEN RETURN; tempR _ IF myData.top.fullScale<0 THEN t*graph.cw ELSE Log10[1 + t]*graph.cw/myData.top.fullScale; myData.top.updateHint _ IF tempR > myData.top.value THEN bigger ELSE IF tempR < myData.top.value THEN smaller ELSE same; myData.top.value _ tempR; tempR _ IF myData.middle.fullScale<0 THEN m*graph.cw ELSE Log10[1 + m]*graph.cw/myData.middle.fullScale; myData.middle.updateHint _ IF tempR > myData.middle.value THEN bigger ELSE IF tempR < myData.middle.value THEN smaller ELSE same; myData.middle.value _ tempR; tempR _ IF myData.bottom.fullScale<0 THEN b*graph.cw ELSE Log10[1 + b]*graph.cw/myData.bottom.fullScale; myData.bottom.updateHint _ IF tempR > myData.bottom.value THEN bigger ELSE IF tempR < myData.bottom.value THEN smaller ELSE same; myData.bottom.value _ tempR; IF NOT graph.parent.iconic THEN ViewerOps.PaintViewer[graph, client, FALSE, $Update]; }; AdjustParameter: Menus.MenuProc = { IF NOT quit THEN { viewer: ViewerClasses.Viewer _ NARROW[parent]; data: Parameter _ NARROW[clientData]; new: INT _ data.value; IF mouseButton = red THEN { IF data.increment = 0 THEN new _ new * 2 ELSE new _ new + data.increment} ELSE { IF data.increment = 0 THEN new _ new / 2 ELSE new _ new - data.increment}; IF new < data.lo THEN new _ data.lo; IF new > data.hi THEN new _ data.hi; IF data.value # new THEN { data.value _ new; data.action[data]; SetLabel[data.label, data.value]}; }; }; SetLabel: PROC [label: NumberLabel, value: INT] = { IF NOT quit AND NOT label.destroyed THEN NumberLabels.NumberLabelUpdate[label, value]; }; FindPosAfter: PROC [base: ROPE, object: ROPE, pos: INT _ 0] RETURNS [INT] = { start: INT _ Rope.Find[s1: base, pos1: pos, s2: object, case: FALSE]; IF start < 0 THEN RETURN [pos]; RETURN [start + object.Length[]]; }; WaitForFSUpdate: ENTRY PROC = {WAIT fsPause}; DisplayFSStatus: PROC [readFSlabel, writeFSlabel, flushFSlabel, NreadFSlabel, NwriteFSlabel, NflushFSlabel: Label] = { readCount: INT _ 0; writeCount: INT _ 0; flushCount: INT _ 0; h: RemoteEventHandle _ NIL; r: ROPE; UNTIL quit DO countLabel, nameLabel: Label _ NIL; count: INT _ 0; style: ATOM _ $BlackOnWhite; WaitForFSUpdate[]; h _ FSBackdoor.NextRemoteEvent[h]; r _ h.fName; IF quit THEN EXIT; SELECT h.op FROM startRetrieving => { nameLabel _ readFSlabel; countLabel _ NreadFSlabel; count _ readCount _ readCount + 1; style _ $WhiteOnBlack; }; endRetrieving => { nameLabel _ readFSlabel; countLabel _ NreadFSlabel; count _ readCount; }; startStoring => { nameLabel _ writeFSlabel; countLabel _ NwriteFSlabel; count _ writeCount _ writeCount + 1; style _ $WhiteOnBlack; }; endStoring => { nameLabel _ writeFSlabel; countLabel _ NwriteFSlabel; count _ writeCount; }; startFlushing => { nameLabel _ flushFSlabel; countLabel _ NflushFSlabel; count _ flushCount _ flushCount + 1; style _ $WhiteOnBlack; }; endFlushing => { nameLabel _ flushFSlabel; countLabel _ NflushFSlabel; count _ flushCount; }; ENDCASE => LOOP; Labels.Set[nameLabel, r.Substr[FindPosAfter[r, DefaultRemoteNames.Get[].systemHost]]]; Labels.SetDisplayStyle[nameLabel, style]; SetLabel[countLabel, count]; ENDLOOP; }; IdleProcess: PROC = TRUSTED { CedarProcess.SetPriority[idle]; WHILE NOT quit DO watchStats.idleCount _ watchStats.idleCount + 1; IF PrincOpsUtils.ReadWDC[] # 0 THEN TRUSTED { DebuggerSwap.WorryCallDebugger["Idle with interrupts disabled?"]; PrincOpsUtils.WriteWDC[0]; }; ENDLOOP; }; CountGFI: PROC RETURNS [free: NAT] = TRUSTED { free _ 0; FOR i: CARDINAL DECREASING IN [1..PrincOps.SD[PrincOps.sGFTLength]) DO item: PrincOps.GFTItem = PrincOps.GFT[i]; IF item.data = 0 THEN free _ free + 1; ENDLOOP; }; CauseGCHit: Menus.MenuProc = TRUSTED { IF NOT quit THEN { viewer: ViewerClasses.Viewer _ NARROW[parent]; Buttons.SetDisplayStyle[viewer, $BlackOnGrey]; SafeStorage.ReclaimCollectibleObjects[TRUE, mouseButton = blue]; IF control OR shift THEN ForceSampleEntry[]; Buttons.SetDisplayStyle[viewer, $BlackOnWhite]; }; }; CollectorWatcher: PROC [gcStatusLabel: Label] = { ENABLE ABORTED => GO TO done; active: ROPE = "BUSY: "; inactive: ROPE = "done."; WHILE NOT quit DO cReasons: ARRAY SafeStorage.ReclamationReason OF ROPE = ["client", "clientTandS", "clientNoTrace", "rcTableOverflow", "allocationInterval", "quantaNeeded", "finalizationThreshold"]; wrds,objs: LONG CARDINAL; GCnumber: INT; msg: ROPE _ inactive; r: SafeStorage.ReclamationReason_ SafeStorage.WaitForCollectorStart[].reason; IF quit THEN EXIT; TRUSTED{ Labels.Set[gcStatusLabel, active.Concat[cReasons[r]] ] }; [incarnation: GCnumber, reason: r, wordsReclaimed: wrds, objectsReclaimed: objs] _ SafeStorage.WaitForCollectorDone[]; IF quit THEN EXIT; Labels.Set[ gcStatusLabel, IO.PutFR[" (GC#%g got %g words, %g objs)", [integer[GCnumber]], [cardinal[wrds]], [cardinal[objs]] ] ]; ENDLOOP; EXITS done => {}; }; SetGCInt: PROC [parm: Parameter] = { [] _ SafeStorage.SetCollectionInterval[parm.value]; }; MyDestroy: ENTRY ViewerClasses.DestroyProc = { ENABLE UNWIND => NULL; viewer: ViewerClasses.Viewer _ NARROW[self]; quit _ TRUE; BROADCAST waitCond; }; WaitForUpdate: ENTRY PROC = { ENABLE UNWIND => NULL; WAIT waitCond; }; SetPause: ENTRY PROC [parm: Parameter] = TRUSTED { ENABLE UNWIND => NULL; Process.SetTimeout[@waitCond, Process.SecondsToTicks[parm.value]]; BROADCAST waitCond; }; ForceSample: Menus.MenuProc = { ENABLE UNWIND => NULL; viewer: ViewerClasses.Viewer _ NARROW[parent]; SELECT mouseButton FROM red => newOpenHeight _ smallerOpenHeight; yellow => newOpenHeight _ middleOpenHeight; blue => newOpenHeight _ biggerOpenHeight; ENDCASE; ForceSampleEntry[]; }; ForceSampleEntry: ENTRY PROC = { ENABLE UNWIND => NULL; millisSinceLastBigBang _ longPause; BROADCAST waitCond; }; ProfileChanged: UserProfile.ProfileChangedProc = { defaultInterval _ UserProfile.Number["Watch.GCInterval", defaultInterval]; defaultSample _ UserProfile.Number["Watch.SamplePause", 2]; longPause _ UserProfile.Number["Watch.LongPause", 30] * 1000; idleMinutesTilPowerOff _ UserProfile.Number["Watch.idleMinutesTilPowerOff", 10]; IF idleMinutesTilPowerOff < 5 THEN idleMinutesTilPowerOff _ 5; IF UserProfile.Boolean["Watch.powerOffInhibit", FALSE] THEN idleMinutesTilPowerOff _ 1000000; powerOffAfter _ UserProfile.Number["Watch.powerOffAfter", 1900]; SELECT powerOffAfter FROM < 0 => powerOffAfter _ 0; > 2400 => powerOffAfter _ 2400; ENDCASE; powerOffBefore _ UserProfile.Number["Watch.powerOffBefore", 700]; SELECT powerOffBefore FROM < 0 => powerOffBefore _ 0; > 2400 => powerOffBefore _ 2400; ENDCASE; idleCpuRate _ UserProfile.Number["Watch.idleCpuRate", 5]*1E-2; SELECT idleCpuRate FROM < 0.0 => idleCpuRate _ 0.0; > 100.0 => idleCpuRate _ 100.0; ENDCASE; minutesSinceTimeSet _ UserProfile.Number["Watch.minutesSinceTimeSet", 15]; IF minutesSinceTimeSet <= 0 THEN minutesSinceTimeSet _ LAST[INT]; }; TestForIdle: PROC [deltaMillis: INT] = { millisSpentIdle _ millisSpentIdle + deltaMillis; WHILE millisSpentIdle >= 60*LONG[1000] DO minutesSpentIdle _ minutesSpentIdle + 1; millisSpentIdle _ millisSpentIdle - 60*LONG[1000]; ENDLOOP; IF Idle.IsIdle[] AND minutesSpentIdle >= idleMinutesTilPowerOff THEN { unpack: BasicTime.Unpacked _ BasicTime.Unpack[BasicTime.Now[]]; minuteTime: INT _ unpack.hour*100+unpack.minute; IF minuteTime > powerOffAfter OR minuteTime < powerOffBefore THEN { IF Booting.CallBootingProcs[] = NIL THEN ProcessorFace.PowerOff[]; }; }; }; BlinkBase: PROC [viewer: ViewerClasses.Viewer] = { DO IF viewer.destroyed THEN EXIT; viewer.icon _ iconArray[0]; IF viewer.iconic THEN ViewerOps.PaintViewer[viewer, all]; Process.Pause[Process.SecondsToTicks[10]]; IF NOT viewer.iconic THEN LOOP; FOR i: NAT IN [0..7] DO viewer.icon _ iconArray[i]; IF viewer.iconic THEN ViewerOps.PaintViewer[viewer, all] ELSE EXIT; Process.Pause[Process.MsecToTicks[100]]; ENDLOOP; FOR i: NAT DECREASING IN [0..7] DO viewer.icon _ iconArray[i]; IF viewer.iconic THEN ViewerOps.PaintViewer[viewer, all] ELSE EXIT; Process.Pause[Process.MsecToTicks[100]]; ENDLOOP; ENDLOOP; }; Watcher: PROC = { ENABLE ABORTED => GO TO done; container, graph: ViewerClasses.Viewer _ NIL; wordsLabel, decayCpuLabel, diskIOLabel: Label _ NIL; diskLabel, gfiLabel, mdsLabel, freeVMLabel, maxFreeLabel: Label _ NIL; minutesIdleLabel: Label _ NIL; millisIdleLabel: Label _ NIL; readFSlabel, writeFSlabel, flushFSlabel: Label _ NIL; NreadFSlabel, NwriteFSlabel, NflushFSlabel: Label _ NIL; diskPercentQueuedLabel, diskActiveSecsLabel, diskTotalSecsLabel: Label _ NIL; diskReadLabel, diskWriteLabel, diskReadPgsLabel, diskWritePgsLabel: Label _ NIL; vmFaultsLabel, vmReadOnlyLabel: Label _ NIL; vmPinnedPagesLabel, vmCheckoutConflictsLabel: Label _ NIL; rmAllocPassesLabel, rmReclamationsLabel: Label _ NIL; rmFreeListLabel, rmOldCleanLabel, rmNewCleanLabel, rmDirtyLabel: Label _ NIL; rmCleanPassesLabel, laundryWakeupsLabel, pagesCleanedLabel: Label _ NIL; panicLaundryWakeupsLabel, pagesCleanedPanicLabel: Label _ NIL; uselessLaundryWakeupsLabel, laundryCleanCallsLabel: Label _ NIL; swapInCallsLabel, swapInVirtualRunsLabel, swapInPhysicalRunsLabel: Label _ NIL; swapInPagesLabel, swapInAlreadyInLabel: Label _ NIL; swapInNoReadLabel, swapInReadsLabel: Label _ NIL; swapInDirtyVictimsLabel, swapInFailedToCleanVictimsLabel: Label _ NIL; Triplet: TYPE = REF TripletRep; TripletRep: TYPE = RECORD [calls, pages, msecs: Label]; fsOpenTriplet: Triplet _ NIL; fsCreateTriplet: Triplet _ NIL; fsDeleteTriplet: Triplet _ NIL; fsExtendTriplet: Triplet _ NIL; fsContractTriplet: Triplet _ NIL; fsReadTriplet: Triplet _ NIL; fsWriteTriplet: Triplet _ NIL; words, wordsRate, oldWordsRate: INT _ 0; oldActiveDiskPulses, oldTotalDiskPulses: BasicTime.Pulses _ 0; oldDiskPercent: REAL _ 0.0; etherStatsData: REF EtherStatsData _ NIL; EtherStatsData: TYPE = RECORD [ ref: EthernetDriverStats.EtherStats _ NIL, old: EthernetDriverStats.EtherStatsRep, time: BasicTime.Pulses, packetsIn, wordsInRate, wordsIn: Labels.Label _ NIL, packetsOut, wordsOutRate, wordsOut: Labels.Label _ NIL]; maxIdleRate: INT _ 1; oldIdleRate, idleRate, lastIdle: INT _ 0; mark: BasicTime.Pulses; millisSinceTimeSet: INT _ 0; pauseParm: Parameter _ NEW[ParameterBlock _ [SetPause, NIL, defaultSample, 0, 1, 64]]; gcParm: Parameter _ NEW[ParameterBlock _ [SetGCInt, NIL, defaultInterval, 0, 1000, 512000]]; SetFileStats: PROC [data: FileStats.Data, trip: Triplet] = { SetLabel[trip.calls, data.calls]; SetLabel[trip.pages, data.pages]; SetLabel[trip.msecs, (BasicTime.PulsesToMicroseconds[data.pulses]+500)/1000]; }; UpdateEtherStats: PROC = { IF etherStatsData # NIL THEN { sample: EthernetDriverStats.EtherStatsRep _ etherStatsData.ref^; time: BasicTime.Pulses _ BasicTime.GetClockPulses[]; delta: REAL _ BasicTime.PulsesToSeconds[time - etherStatsData.time]; SetLabel[etherStatsData.packetsIn, sample.packetsRecv]; SetLabel[etherStatsData.wordsInRate, Real.Round[(sample.wordsRecv - etherStatsData.old.wordsRecv)/delta]]; SetLabel[etherStatsData.wordsIn, sample.wordsRecv]; SetLabel[etherStatsData.packetsOut, sample.packetsSent]; SetLabel[etherStatsData.wordsOutRate, Real.Round[(sample.wordsSent - etherStatsData.old.wordsSent)/delta]]; SetLabel[etherStatsData.wordsOut, sample.wordsSent]; etherStatsData.old _ sample; etherStatsData.time _ time; }; }; BuildTool: PROC = { graphClass: ViewerClasses.ViewerClass = NEW[ViewerClasses.ViewerClassRec _ [paint: GraphPaint, destroy: MyDestroy]]; gapX: INTEGER _ 2; gapY: INTEGER _ 0; lastX: INTEGER _ 0; nextX: INTEGER _ gapX; nextY: INTEGER _ 0; AddLabel: PROC [prefix: ROPE, chars: NAT _ 0, lastInLine: BOOL _ FALSE, ww: INTEGER _ 0] RETURNS [label: Label] = { IF chars # 0 THEN { label _ Labels.Create[ info: [name: prefix, parent: container, border: FALSE, wx: nextX, wy: nextY, ww: ww], paint: FALSE]; nextX _ nextX + label.ww; label _ NumberLabels.CreateNumber[ info: [name: NIL, parent: container, border: FALSE, wx: nextX, wy: nextY], chars: chars, paint: FALSE]; } ELSE label _ Labels.Create [ info: [name: prefix, parent: container, border: FALSE, wx: nextX, wy: nextY, ww: ww], paint: FALSE]; lastX _ nextX _ label.wx + label.ww + gapX; IF lastInLine THEN { nextX _ gapX; nextY _ label.wy + label.wh + gapY}; }; AddTriplet: PROC [lead: ROPE, ww: INTEGER] RETURNS [trip: Triplet] = { IF lead # NIL THEN [] _ AddLabel[lead, 0, FALSE, ww]; trip _ NEW[TripletRep _ [AddLabel["calls", 6], NIL, NIL]]; trip.pages _ AddLabel["pages", 7]; trip.msecs _ AddLabel["msecs", 8, TRUE]; }; AddButton: PROC [buttonName: ROPE _ NIL, chars: NAT _ 0, parm: Parameter _ NIL, proc: Buttons.ButtonProc _ NIL, lastInLine: BOOL _ FALSE] RETURNS [button: Buttons.Button] = { label: ViewerClasses.Viewer _ NIL; IF proc = NIL THEN proc _ AdjustParameter; button _ Buttons.Create [ info: [name: buttonName, parent: container, wx: nextX, wy: nextY, border: TRUE], fork: TRUE, proc: proc, clientData: parm, paint: FALSE]; nextX _ nextX + button.ww; SELECT TRUE FROM chars # 0 => label _ NumberLabels.CreateNumber[ info: [name: NIL, parent: container, border: FALSE, wx: nextX, wy: nextY], chars: chars, paint: FALSE]; parm # NIL => label _ Labels.Create [ info: [name: NIL, parent: container, border: FALSE, wx: nextX, wy: nextY], paint: FALSE]; ENDCASE; IF label # NIL THEN { nextX _ nextX + label.ww; IF parm # NIL THEN parm.label _ label; }; lastX _ nextX _ nextX + gapX; IF lastInLine THEN { nextX _ gapX; nextY _ button.wy + button.wh + gapY}; }; SimpleLabel: PROC [name: ROPE, wx,wy: INTEGER] = { [] _ Labels.Create[info: [name: name, parent: container, wx: wx, wy: wy, border: FALSE], paint: FALSE]; }; AddEtherStats: PROC = { ref: EthernetDriverStats.EtherStats _ EthernetDriverStats.GetEthernetOneStats[0]; IF ref = NIL THEN ref _ EthernetDriverStats.GetEthernetStats[0]; IF ref = NIL THEN RETURN; etherStatsData _ NEW[EtherStatsData]; etherStatsData.ref _ ref; etherStatsData.old _ ref^; etherStatsData.time _ BasicTime.GetClockPulses[]; etherStatsData.packetsIn _ AddLabel["Ether packets in", 10]; etherStatsData.wordsInRate _ AddLabel["wds/sec", 6]; etherStatsData.wordsIn _ AddLabel["words", 11, TRUE]; etherStatsData.packetsOut _ AddLabel["Ether packets out", 9]; etherStatsData.wordsOutRate _ AddLabel["wds/sec", 6]; etherStatsData.wordsOut _ AddLabel["words", 11, TRUE]; }; CreateGraph: PROC RETURNS[viewer: ViewerClasses.Viewer] = { W2: PROC [r: ROPE] RETURNS [INTEGER] = {RETURN[VFonts.StringWidth[r]/2]}; xTemp: INTEGER; yTemp: INTEGER; wordsLabel _ AddLabel["Words", 9, TRUE]; xTemp _ lastX; decayCpuLabel _ AddLabel["CPU Load", 3, TRUE]; xTemp _ MAX[xTemp, lastX]; diskIOLabel _ AddLabel["DiskIO", 9, TRUE]; xTemp _ MAX[xTemp, lastX]; viewer _ ViewerOps.CreateViewer[ flavor: $BarGraph, info: [parent: container, wx: xTemp, wy: wordsLabel.wy + wordsLabel.wh, ww: ViewerSpecs.openRightWidth - xTemp - 5, wh: diskIOLabel.wy - (wordsLabel.wy + wordsLabel.wh), data: NEW[GraphDataRec _ [[fullScale: 5], [fullScale: -1], [fullScale: -1]]]], paint: FALSE]; xTemp _ viewer.ww/5; yTemp _ wordsLabel.wy; SimpleLabel["1", viewer.wx - W2["1"], yTemp]; SimpleLabel["10", viewer.wx + xTemp - W2["10"], yTemp]; SimpleLabel["100", viewer.wx + 2*xTemp - W2["100"], yTemp]; SimpleLabel["1000", viewer.wx + 3*xTemp - W2["1000"], yTemp]; SimpleLabel["10000", viewer.wx + 4*xTemp - W2["10000"], yTemp]; xTemp _ viewer.ww/4; yTemp _ diskIOLabel.wy; SimpleLabel["0%", viewer.wx - W2["1"], yTemp]; SimpleLabel["25%", viewer.wx + xTemp - W2["3"], yTemp]; SimpleLabel["50%", viewer.wx + xTemp*2 - W2["10"], yTemp]; SimpleLabel["75%", viewer.wx + xTemp*3 - W2["30"], yTemp]; }; container _ Containers.Create[info: [name: "Watch", iconic: TRUE, icon: iconArray[0], column: right, scrollable: FALSE]]; ViewerOps.RegisterViewerClass[$BarGraph, graphClass]; graph _ CreateGraph[]; [] _ AddLabel["Free "]; diskLabel _ AddLabel["disk", 6]; gfiLabel _ AddLabel["gfi", 3]; mdsLabel _ AddLabel["mds", 3]; freeVMLabel _ AddLabel["VM", 5]; maxFreeLabel _ AddLabel["VM run", 5, TRUE]; gapY _ 1; [] _ AddButton[buttonName: "GC interval", chars: 6, parm: gcParm]; SetLabel[gcParm.label, gcParm.value]; SetGCInt[gcParm]; [] _ AddButton[buttonName: "GC", proc: CauseGCHit]; { gcStatusLabel: Label = AddLabel["inactive.", 0, TRUE, ViewerSpecs.openRightWidth]; Containers.ChildXBound[container, gcStatusLabel]; TRUSTED {Process.Detach[FORK CollectorWatcher[gcStatusLabel]]}; }; [] _ AddButton[buttonName: "Sample", proc: ForceSample]; [] _ AddButton[buttonName: "interval", chars: 2, parm: pauseParm]; SetLabel[pauseParm.label, pauseParm.value]; SetPause[pauseParm]; readFSlabel _ AddLabel[ "FS status (inverted iff busy)", 0, TRUE, ViewerSpecs.openRightWidth]; Containers.ChildXBound[container, readFSlabel]; middleOpenHeight _ readFSlabel.wy + readFSlabel.wh * MAX[LONG[1], MIN[LONG[30], UserProfile.Number["Watch.middleLines", 2]]+1]; newOpenHeight _ oldOpenHeight _ smallerOpenHeight _ nextY; ViewerOps.SetOpenHeight[container, oldOpenHeight]; gapY _ 0; [] _ AddLabel["Flushing "]; flushFSlabel _ AddLabel[ "(FS file being flushed)", 0, TRUE, ViewerSpecs.openRightWidth]; Containers.ChildXBound[container, flushFSlabel]; [] _ AddLabel["Storing "]; writeFSlabel _ AddLabel[ "(FS file being stored)", 0, TRUE, ViewerSpecs.openRightWidth]; Containers.ChildXBound[container, writeFSlabel]; [] _ AddLabel["FS "]; NreadFSlabel _ AddLabel["fetches", 5]; NflushFSlabel _ AddLabel["flushes", 5]; NwriteFSlabel _ AddLabel["stores", 5, TRUE]; AddEtherStats[]; minutesIdleLabel _ AddLabel["Idle minutes", 5]; millisIdleLabel _ AddLabel["millis", 5, TRUE]; [] _ AddLabel["Disk "]; diskPercentQueuedLabel _ AddLabel["% busy", 3]; diskActiveSecsLabel _ AddLabel["secs busy", 6]; diskTotalSecsLabel _ AddLabel["secs total", 7, TRUE]; diskReadLabel _ AddLabel[" reads", 6]; diskReadPgsLabel _ AddLabel["rPgs", 7]; diskWriteLabel _ AddLabel["writes", 6]; diskWritePgsLabel _ AddLabel["wPgs", 7, TRUE]; [] _ AddLabel["VM "]; vmFaultsLabel _ AddLabel["faults", 7]; vmReadOnlyLabel _ AddLabel["readOnly", 5]; vmPinnedPagesLabel _ AddLabel["pinned", 5]; vmCheckoutConflictsLabel _ AddLabel["conflicts", 4, TRUE]; [] _ AddLabel["Replacement "]; rmAllocPassesLabel _ AddLabel["passes", 4]; rmReclamationsLabel _ AddLabel["pages", 6, TRUE]; rmFreeListLabel _ AddLabel[" free", 6]; rmOldCleanLabel _ AddLabel["old", 6]; rmNewCleanLabel _ AddLabel["new", 6]; rmDirtyLabel _ AddLabel["dirty", 6, TRUE]; [] _ AddLabel["Laundry "]; rmCleanPassesLabel _ AddLabel["passes", 3]; pagesCleanedLabel _ AddLabel["pages", 7]; laundryWakeupsLabel _ AddLabel["wakeups", 8, TRUE]; panicLaundryWakeupsLabel _ AddLabel[" panic", 3]; pagesCleanedPanicLabel _ AddLabel["panicPgs", 4]; laundryCleanCallsLabel _ AddLabel["cleanCalls", 5]; uselessLaundryWakeupsLabel _ AddLabel["useless", 8, TRUE]; [] _ AddLabel["SwapIn "]; swapInCallsLabel _ AddLabel["calls", 6]; swapInVirtualRunsLabel _ AddLabel["vRuns", 6]; swapInPhysicalRunsLabel _ AddLabel["pRuns", 6, TRUE]; swapInPagesLabel _ AddLabel[" pages", 7]; swapInAlreadyInLabel _ AddLabel["alreadyIn", 7]; swapInNoReadLabel _ AddLabel["undef", 7]; swapInReadsLabel _ AddLabel["read", 7, TRUE]; swapInDirtyVictimsLabel _ AddLabel[" dirtyVictims", 5]; swapInFailedToCleanVictimsLabel _ AddLabel["cleanFailed", 5, TRUE]; {-- line 11: FileStats stuff ww: INTEGER; [] _ AddLabel["File Statistics", 0, TRUE]; ww _ AddLabel[" Open "].ww; fsOpenTriplet _ AddTriplet[NIL, ww]; fsCreateTriplet _ AddTriplet[" Create", ww]; fsDeleteTriplet _ AddTriplet[" Delete", ww]; fsExtendTriplet _ AddTriplet[" Extend", ww]; fsContractTriplet _ AddTriplet[" Contract", ww]; fsReadTriplet _ AddTriplet[" Read", ww]; fsWriteTriplet _ AddTriplet[" Write", ww]; }; biggerOpenHeight _ nextY; mark _ BasicTime.GetClockPulses[]; watchStats.idleCount _ 0; words _ SafeStorage.NWordsAllocated[]; TRUSTED { Process.Detach[FORK IdleProcess[]]; Process.Detach[FORK DisplayFSStatus[readFSlabel, writeFSlabel, flushFSlabel, NreadFSlabel, NwriteFSlabel, NflushFSlabel]]; }; }; BuildTool[]; TRUSTED { Process.Detach[FORK BlinkBase[container]]; }; WHILE NOT quit AND NOT container.destroyed DO reads,writes,readPgs,writePgs: INT; diskPercent: REAL _ 0.0; deltaMillis: LONG CARDINAL; diskIO: INT _ 0; { nextMark: BasicTime.Pulses = BasicTime.GetClockPulses[]; idleTemp: INT _ watchStats.idleCount; delta: LONG CARDINAL _ BasicTime.PulsesToMicroseconds[nextMark - mark]; deltaMillis _ (delta + 500) / 1000; IF deltaMillis <= 10 THEN { Process.Pause[1]; LOOP}; mark _ nextMark; idleRate _ (idleTemp-lastIdle) / deltaMillis; lastIdle _ idleTemp; IF idleRate > maxIdleRate THEN IF deltaMillis > 100 THEN maxIdleRate _ idleRate ELSE idleRate _ maxIdleRate; }; { newActiveDiskPulses, newTotalDiskPulses: BasicTime.Pulses; [active: newActiveDiskPulses, total: newTotalDiskPulses, reads: reads, writes: writes, readPages: readPgs, writePages: writePgs] _ Disk.GetStatistics[]; diskIO _ reads+writes; IF newTotalDiskPulses # oldTotalDiskPulses THEN { IF newActiveDiskPulses # oldActiveDiskPulses THEN { diskPercent _ (1.0*(newActiveDiskPulses-oldActiveDiskPulses)) / (newTotalDiskPulses-oldTotalDiskPulses); oldActiveDiskPulses _ newActiveDiskPulses; }; oldTotalDiskPulses _ newTotalDiskPulses; }; }; { deltaWords: INT _ SafeStorage.NWordsAllocated[] - words; words _ words + deltaWords; wordsRate _ (deltaWords * 1000 + 500) / deltaMillis; }; { frac: REAL _ deltaMillis * 1E-4; IF frac > 1.0 THEN frac _ 1.0; watchStats.cpuLoad _ 1 - idleRate/(maxIdleRate*1.0); decayCpuRate _ (1.0-frac)*decayCpuRate + frac*watchStats.cpuLoad; }; millisSinceLastBigBang _ MIN[longPause, millisSinceLastBigBang + deltaMillis]; millisSinceTimeSet _ millisSinceTimeSet + deltaMillis; IF decayCpuRate < idleCpuRate AND diskPercent = 0.0 AND wordsRate = 0 THEN { TestForIdle[deltaMillis]; IF millisSinceTimeSet/60000 > minutesSinceTimeSet THEN { BasicTime.SetTime[]; millisSinceTimeSet _ 0; }; } ELSE minutesSpentIdle _ millisSpentIdle _ 0; IF quit OR container.destroyed THEN GO TO done; IF container.iconic OR Idle.IsIdle[] THEN { Process.Pause[Process.SecondsToTicks[2]]; LOOP; }; { pagesAllocated, pagesFreed, pagesInPartition: VM.PageCount; TRUSTED { temp: INT; [pagesAllocated, pagesFreed, pagesInPartition] _ VMStatistics.VirtualAllocation[mds]; temp _ MAX[0, pagesInPartition - pagesAllocated + pagesFreed]; watchStats.mdsFree _ temp; [pagesAllocated, pagesFreed, pagesInPartition] _ VMStatistics.VirtualAllocation[normalVM]; temp _ MAX[0, pagesInPartition - pagesAllocated + pagesFreed]; watchStats.vmFree _ temp; }; IF millisSinceLastBigBang >= longPause AND NOT container.iconic THEN { millisSinceLastBigBang _ 0; [] _ VM.Allocate[pagesInPartition+pagesInPartition ! VM.CantAllocate => {watchStats.vmRun _ bestInterval.count; CONTINUE}; ]; watchStats.gfiFree _ CountGFI[]; mark _ BasicTime.GetClockPulses[]; lastIdle _ watchStats.idleCount; }; }; IF newOpenHeight # oldOpenHeight THEN { ViewerOps.SetOpenHeight[container, oldOpenHeight _ newOpenHeight]; IF NOT container.iconic THEN ViewerOps.ComputeColumn[ViewerOps.ViewerColumn[container]]; }; SetLabel[diskLabel, watchStats.diskFree _ FileBackdoor.GetVolumePages[File.SystemVolume[]].free]; SetLabel[mdsLabel, watchStats.mdsFree]; SetLabel[gfiLabel, watchStats.gfiFree]; SetLabel[freeVMLabel, watchStats.vmFree]; SetLabel[maxFreeLabel, watchStats.vmRun]; { SetLabel[wordsLabel, words]; SetLabel[diskIOLabel, diskIO]; IF oldDiskPercent # diskPercent OR oldWordsRate # wordsRate OR oldIdleRate # idleRate THEN { oldIdleRate _ idleRate; GraphSet[ graph, oldWordsRate _ wordsRate, watchStats.cpuLoad, oldDiskPercent _ diskPercent]; }; SetLabel[decayCpuLabel, Real.RoundI[decayCpuRate*100.0]]; }; IF container.wh > smallerOpenHeight THEN { UpdateEtherStats[]; SetLabel[minutesIdleLabel, minutesSpentIdle]; SetLabel[millisIdleLabel, millisSpentIdle]; SetLabel[diskPercentQueuedLabel, Real.RoundI[oldDiskPercent*100.0]]; SetLabel[ diskActiveSecsLabel, (BasicTime.PulsesToMicroseconds[oldActiveDiskPulses]+500000)/1000000]; SetLabel[ diskTotalSecsLabel, (BasicTime.PulsesToMicroseconds[oldTotalDiskPulses]+500000)/1000000]; SetLabel[diskReadLabel, reads]; SetLabel[diskWriteLabel, writes]; SetLabel[diskReadPgsLabel, readPgs]; SetLabel[diskWritePgsLabel, writePgs]; SetLabel[vmFaultsLabel, VMStatistics.pageFaults]; SetLabel[vmReadOnlyLabel, VMStatistics.readOnlyPages]; SetLabel[vmPinnedPagesLabel, VMStatistics.pinnedPages]; SetLabel[vmCheckoutConflictsLabel, VMStatistics.checkoutConflicts]; SetLabel[rmAllocPassesLabel, VMStatistics.rmAllocPasses]; SetLabel[rmReclamationsLabel, VMStatistics.rmReclamations]; SetLabel[rmFreeListLabel, VMStatistics.rmFreeList]; SetLabel[rmOldCleanLabel, VMStatistics.rmOldClean]; SetLabel[rmNewCleanLabel, VMStatistics.rmNewClean]; SetLabel[rmDirtyLabel, VMStatistics.rmDirty]; SetLabel[rmCleanPassesLabel, VMStatistics.rmCleanPasses]; SetLabel[laundryWakeupsLabel, VMStatistics.laundryWakeups]; SetLabel[pagesCleanedLabel, VMStatistics.pagesCleaned]; SetLabel[panicLaundryWakeupsLabel, VMStatistics.panicLaundryWakeups]; SetLabel[pagesCleanedPanicLabel, VMStatistics.pagesCleanedPanic]; SetLabel[uselessLaundryWakeupsLabel, VMStatistics.uselessLaundryWakeups]; SetLabel[laundryCleanCallsLabel, VMStatistics.laundryCleanCalls]; SetLabel[swapInCallsLabel, VMStatistics.swapInCalls]; SetLabel[swapInVirtualRunsLabel, VMStatistics.swapInVirtualRuns]; SetLabel[swapInPhysicalRunsLabel, VMStatistics.swapInPhysicalRuns]; SetLabel[swapInPagesLabel, VMStatistics.swapInPages]; SetLabel[swapInAlreadyInLabel, VMStatistics.swapInAlreadyIn]; SetLabel[swapInNoReadLabel, VMStatistics.swapInNoRead]; SetLabel[swapInReadsLabel, VMStatistics.swapInReads]; SetLabel[swapInDirtyVictimsLabel, VMStatistics.swapInDirtyVictims]; SetLabel[swapInFailedToCleanVictimsLabel, VMStatistics.swapInFailedToCleanVictims]; SetFileStats[FileStats.GetData[open], fsOpenTriplet]; SetFileStats[FileStats.GetData[create], fsCreateTriplet]; SetFileStats[FileStats.GetData[delete], fsDeleteTriplet]; SetFileStats[FileStats.GetData[extend], fsExtendTriplet]; SetFileStats[FileStats.GetData[contract], fsContractTriplet]; SetFileStats[FileStats.GetData[read], fsReadTriplet]; SetFileStats[FileStats.GetData[write], fsWriteTriplet]; }; WaitForUpdate[]; ENDLOOP; GO TO done; EXITS done => {quit _ TRUE}; }; TRUSTED { iconFileName: ROPE _ FS.ExpandName["Watch.icons"].fullFName; iconArray _ NEW[IconArrayRep _ ALL[tool]]; FOR i: NAT IN [0..7] DO iconArray[i] _ Icons.NewIconFromFile[iconFileName, i ! FS.Error => EXIT]; ENDLOOP; UserProfile.CallWhenProfileChanges[ProfileChanged]; Process.Detach[FORK Watcher]; }; END. òWatch.mesa Copyright c 1984, 1985, 1986 by Xerox Corporation. All rights reserved. Doug Wyatt, April 25, 1985 0:29:36 am PST Russ Atkinson (RRA) May 10, 1986 1:12:58 pm PDT **** Useful types from other interfaces **** **** Useful local types and constants **** **** The following stats are exported via GetWatchStats **** **** Global variables for Watch **** **** Procedures for Watch **** Log10[x] quickly returns x < 1 => 0 x IN [1..1e6] => log(x) {absolute error < .0007} x > 1e6 => 6 algorithm from Abramowitz: Handbook of Math Functions, p. 68 fast scale to [1..10], biased toward small sizes scale to [1..1/sqrt10] magic cubic approximation [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL] There is only one idle process in the system at a time, and the idle process is the only one that runs at priority 0. The idle process checks for page faults while interrupts are disabled by looking at the wakeup disable counter. Interrupts have been found to be disabled. This is often due to some joker disabling interrupts and getting an address fault. If the person exaining this situation finds it to be benign, then proceeding this call to the debugger will enable interrupts and allow some salvage work to go on. Enable interrupts. This procedure counts the free GFIs via a linear scan of the table. [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL] [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL] [parent: REF, clientData: REF, mouseButton: MouseButton, shift, control: BOOL] [reason: UserProfile.ProfileChangeReason] We have been "idle" for during the last interval, so test for it being enough. It is the right time, and we have been idle for long enough, so let's power off the machine to save power. Free line labels Idle labels FS watcher labels Disk read/write labels VM labels Replacement labels Laundry labels SwapIn labels FileStats labels (stored as triplets) We have some ether stats to display BuildTool is a separate procedure to factor out the work done to create a viewer from the work done to refresh the Watch tool. This is especially important to reduce the liklihood of overflowing storage in Pass 5 of the compiler, I think. Must do this here, since order of evaluation matters build enclosing viewer line 0: bar graphs line 1: mds, gfi, disk, freeVM line 2: gc interval, button & status line 3: sample button and FS status set an aesthetically sized open window Add labels below this line for the BIG size of Watch. line 4: file being flushed line 5: file being stored line 6: FS counts line 6A1: Ether stats line 6B: Idle minutes line 7: Disk stats line 8: VM stats line 9: replacement stats line 10: laundry stats line 11: SwapIn stats initialize measurments Now we come to the main Watcher loop Get the time delta Make this follow the sampling of GetClockPulses as quickly as possible Not enough precision, so wait for the minimum time, then loop Update the idle rate data If enough time has elapsed, then use new (higher) idle rate. If the test interval is too short, then don't trust the new idle rate (error is likely to be too high). Update the disk numbers Update the alloc data Maintain the exponentially decaying CPU rate. Every 15 minutes we should reset the time to keep it from drifting too far Test for a valid viewer still existing The user really does not want to see the data now. We performed the above calculations just to keep the data current (and to avoid overflow in some numbers). Sample the new VM & GFI numbers Calculate the stats for the max VM run (but only if they will be displayed). We do this infrequently since it it relatively expensive. Calculate the # of GFIs free To avoid a spike in the CPU usage (which can make it look like we are not idle when we really are), we sample the idle count and clock pulses again here. The desired open height has changed, so actually make the change here. Display Free line: disk, mds, gfi, VM, VM run Display gc interval, button & status & CPU avg. rate There is more than the minimum amount of information on the screen, so we try to update those numbers as well. Idle info Idle info Disk stats VM stats Replacement stats Laundry stats SwapIn stats FileStats stuff Lastly, wait for the pause interval **** Initialization code Ê"טcodešœ ™ Kšœ Ïmœ=™HK™)K™/—™šÏk ˜ Kšœ žœa˜pKšœžœ˜"Kšœžœ0˜=Kšœ žœ˜!Kšœ žœ˜'Kšœ žœ˜'Kšœžœ˜Kšœžœ˜KšœžœD˜]Kšœžœ˜Kšœ žœ˜$Kšœ žœ˜ Kšžœžœ˜Kšœ žœ*˜:Kšœžœ4˜@Kšœžœ'˜3Kšœžœ ˜Kšœžœ˜*Kšžœžœ ˜Kšœžœ ˜Kšœ žœ0˜BKšœ žœžœžœ˜9Kšœžœ˜(Kšœžœ:˜GKšœžœ ˜Kšœžœ˜Kšœžœžœ ˜0Kšœ žœ…˜–Kšœ žœ?˜PKšœžœ˜Kšœžœ@˜SKšœ žœ_˜nKšœ žœ˜#Kšžœžœ%˜-Kšœ žœ¸˜Êšœ žœ˜$K˜———šÐblœžœž˜Kšžœ”žœ+žœ}žœ˜×Kšžœ ˜Kšœž˜—K˜šœ,™,Kšœžœ˜Kšœžœ˜Kšœ žœ˜-Kšœžœžœžœ˜>Kšœ žœ˜%Kšžœžœžœ˜—K˜šœ*™*Kšœ žœžœ˜#šœ žœžœ˜KšœžœÏc˜*K˜!Kšœ žœ 2˜D—Kšœ žœ˜,Kšœžœžœ$˜@Kšœ žœžœ˜%šœžœžœ˜Kšœžœ ˜Kšœ ˜ Kšœžœ˜—šœ žœžœ˜#Kšœžœžœžœ˜6——K˜šœ<™˜>Kšœ˜—šœ ˜ Kšœ(˜(KšœD˜DKšœ˜—Kšžœ˜—Kšœ&˜&K˜K˜—š ¡œžœžœžœžœ˜,šœ™K™ Kšœžœ,™0Kšœ ™ —Kšœ<™K˜@——Kšœ žœžœ˜Kšœ žœ˜Kšœžœ ˜K˜NKšžœžœžœ˜Kšžœ<˜C˜SK˜#—Kšžœžœžœ˜šœ ˜ Kšœ˜šžœ(˜*Kšœ9˜9—Kšœ˜—Kšžœ˜—Kšžœ ˜K˜K˜—š¡œžœ˜$K˜3K˜K˜—šœ žœ˜.Kšžœžœžœ˜Kšœžœ˜,Kšœžœ˜ Kšž œ ˜K˜K˜—š¡ œžœžœ˜Kšžœžœžœ˜Kšžœ ˜˜K˜——š¡œžœžœžœ˜2Kšžœžœžœ˜K˜BKšž œ ˜K˜K˜—šœ˜Kšœ žœžœ,žœ™NKšžœžœžœ˜Kšœžœ ˜.šžœ ž˜Kšœ)˜)Kšœ+˜+Kšœ)˜)Kšžœ˜—Kšœ˜K˜K˜—š¡œžœžœ˜ Kšœ žœžœ,žœ™NKšžœžœžœ˜Kšœ#˜#Kšž œ ˜K˜K˜—•StartOfExpansion- -- [reason: UserProfile.ProfileChangeReason]šœ2˜2Kšœ)™)KšœJ˜JKšœ;˜;Kšœ=˜=K˜PKšžœžœ˜>šžœ.žœž˜;Kšœ!˜!—K˜@šžœž˜Kšœ˜Kšœ˜Kšžœ˜—K˜Ašžœž˜Kšœ˜Kšœ ˜ Kšžœ˜—K˜>šžœ ž˜Kšœ˜Kšœ˜Kšžœ˜—K˜JKšžœžœžœžœ˜AKšœ˜K˜—š¡ œžœžœ˜(KšœO™OKšœ0˜0šžœžœž˜)Kšœ(˜(Kšœ'žœ˜2Kšžœ˜—šžœžœ,žœ˜FK˜?Kšœ žœ!˜0šžœžœžœ˜CKšœj™jKšžœžœžœ˜BK˜—K˜—K˜K˜—š¡ œžœ#˜2šž˜Kšžœžœžœ˜Kšœ˜Kšžœžœ$˜9Kšœ*˜*Kšžœžœžœžœ˜K˜šžœžœžœž˜Kšœ˜Kšžœžœ$žœžœ˜CKšœ(˜(Kšžœ˜—š žœžœž œžœž˜"Kšœ˜Kšžœžœ$žœžœ˜CKšœ(˜(Kšžœ˜—Kšžœ˜—K˜K˜—š¡œžœ˜Kšžœžœžœžœ˜Kšœ)žœ˜-Kšœ0žœ˜4šœ™KšœBžœ˜F—šœ ™ Kšœžœ˜Kšœžœ˜—šœ™Kšœ1žœ˜5Kšœ4žœ˜8—šœ™KšœIžœ˜MKšœLžœ˜P—šœ ™ Kšœ(žœ˜,Kšœ6žœ˜:—šœ™Kšœ1žœ˜5KšœIžœ˜M—šœ™KšœDžœ˜HKšœ:žœ˜>Kšœ<žœ˜@—šœ ™ KšœKžœ˜OKšœ0žœ˜4Kšœ-žœ˜1KšœBžœ˜F—šœ%™%Kšœ žœžœ ˜Kšœ žœžœ˜7Kšœžœ˜Kšœžœ˜Kšœžœ˜Kšœžœ˜Kšœžœ˜!Kšœžœ˜Kšœžœ˜—Kšœ žœ˜(K˜>Kšœžœ˜K˜šœžœžœ˜)šœžœžœ˜Kšœ&žœ˜*Kšœ'˜'Kšœ˜Kšœ0žœ˜4Kšœ3žœ˜8——K˜Kšœ žœ˜Kšœ!žœ˜)Kšœ˜Kšœžœ˜K˜˜Kšžœžœ˜?—˜Kšžœžœ%˜HK˜—š¡ œžœ*˜Kšœ˜šœ.˜.Kšœ+˜+—Kšœžœ4˜>Kšœ˜K˜—šžœ%žœžœžœ˜FKšœ‡™‡Kšœ˜šœžœ+˜2Kšœžœ9žœ˜GKšœ˜—Kšœ™Kšœ ˜ Kšœ™™™Kšœ"˜"Kšœ ˜ K˜—Kšœ˜—K˜šžœžœ˜'KšœF™FKšœB˜Bšžœžœž˜Kšœ;˜;—K˜—K™Kšœ-™-Kšœa˜aKšœ'˜'Kšœ'˜'Kšœ)˜)Kšœ)˜)K™šœ˜Kšœ4™4K˜K˜šžœžœžœžœ˜\Kšœ˜˜ K˜K˜Kšœ˜Kšœ˜—K˜—Kšœ9˜9K˜—K˜šžœ"žœ˜*Kšœn™nK˜Kšœ ™ Kšœ˜K˜Kšœ ™ Kšœ-˜-Kšœ+˜+K˜Kšœ ™ KšœD˜Dšœ ˜ Kšœ˜KšœF˜F—šœ ˜ Kšœ˜KšœE˜E—Kšœ˜Kšœ!˜!Kšœ$˜$Kšœ&˜&K˜Kšœ™Kšœ1˜1Kšœ6˜6Kšœ7˜7KšœC˜CK˜Kšœ™Kšœ9˜9Kšœ;˜;Kšœ3˜3Kšœ3˜3Kšœ3˜3Kšœ-˜-K˜Kšœ ™ Kšœ9˜9Kšœ;˜;Kšœ7˜7KšœE˜EKšœA˜AKšœI˜IKšœA˜AK˜Kšœ ™ Kšœ5˜5KšœA˜AKšœC˜CKšœ5˜5Kšœ=˜=Kšœ7˜7Kšœ5˜5KšœC˜CKšœS˜SK˜Kšœ™Kšœ5˜5Kšœ9˜9Kšœ9˜9Kšœ9˜9Kšœ=˜=Kšœ5˜5šœ7˜7K˜—K˜—K™Kšœ#™#K˜K˜Kšžœ˜K˜—Kšžœžœ˜ Kšžœžœ˜K˜——K˜šœ™K™šžœ˜ Kšœžœžœ%˜