DIRECTORY YggEnvironment, YggInternal, BasicTime, YggDIDMap USING [DocumentFromLockID, GetName], List, YggLock, YggLockControl, YggLockInternal, Process, Rope USING [Cat, Concat, ROPE], YggMonitoringHooks USING [TransIDFromTransHandle, TransIDToRope], YggMonitoringLog USING [notice, TransactionAbortInfo], YggTransactionMap; YggLockWatchdogImpl: CEDAR PROGRAM IMPORTS BasicTime, YggDIDMap, List, YggLock, YggLockInternal, Process, Rope, YggMonitoringHooks, YggMonitoringLog, YggTransactionMap EXPORTS YggInternal, YggLockControl = BEGIN LockID: TYPE = YggLock.LockID; nullLockID: LockID = YggLock.nullLockID; LockMode: TYPE = YggLock.LockMode; ModeReleasableSet: TYPE = YggLock.ModeReleasableSet; Lock: TYPE = YggLockInternal.Lock; LockRep: TYPE = YggLockInternal.LockRep; Header: TYPE = YggLockInternal.Header; Request: TYPE = YggLockInternal.Request; GrantedRequest: TYPE = YggLockInternal.GrantedRequest; WaitingRequest: TYPE = YggLockInternal.WaitingRequest; LockTransHeader: TYPE = YggLockInternal.LockTransHeader; ConflictingTranses: TYPE = RECORD[holder: YggTransactionMap.TransHandle, waiter: YggTransactionMap.TransHandle, lock: LockID]; -- typical information about a lock conflict LockTransHeaderObject: PUBLIC TYPE = YggLockInternal.LockRep.request.transHeader; ForkWatchdogProcess: PUBLIC PROC [ wakeupPeriod: Process.Milliseconds, abortWaitingRequestInterval: INT--seconds--, abortInactiveGrantedRequestInterval: INT--seconds--] = { TRUSTED {Process.Detach[FORK LockWatchdogProcess[wakeupPeriod, abortWaitingRequestInterval, abortInactiveGrantedRequestInterval]]; }; }; LockWatchdogProcess: PROC [ wakeupPeriod: Process.Milliseconds, abortWaitingRequestInterval: INT--seconds--, abortInactiveGrantedRequestInterval: INT--seconds--] = { wakeupPeriodsUntilTimeoutCheck: INT _ 0; numWaits: INT _ 0; p: Path _ NIL; DO Process.Pause[Process.MsecToTicks[wakeupPeriod] ! ABORTED => GOTO aborted]; IF (wakeupPeriodsUntilTimeoutCheck _ wakeupPeriodsUntilTimeoutCheck-1) <= 0 THEN { l: LIST OF ConflictingTranses; logProc: PROC [YggMonitoringLog.TransactionAbortInfo]; -- (used to stop race condition) [wakeupPeriodsUntilTimeoutCheck, l] _ ChooseTimeoutVictims[wakeupPeriod, abortWaitingRequestInterval]; UNTIL l = NIL DO lNext: LIST OF ConflictingTranses _ l.rest; IF (logProc _ YggMonitoringLog.notice.abortTransaction) # NIL THEN { fileName: Rope.ROPE; -- name of the file being fought over message: Rope.ROPE; message _ IF l.first.holder # NIL THEN Rope.Cat["transaction timed out on lock held by ", YggMonitoringHooks.TransIDToRope[YggMonitoringHooks.TransIDFromTransHandle[l.first.holder]]] ELSE "transaction timed out; livelock?"; fileName _ YggDIDMap.GetName[YggDIDMap.DocumentFromLockID[l.first.lock]]; message _ message.Cat[ "; waiting for ", IF fileName = NIL THEN "(NIL)" ELSE fileName ]; logProc[[ transID: YggMonitoringHooks.TransIDFromTransHandle[l.first.waiter], where: "LockWatchdogImpl.LockWatchdogProcess", why: watchDog, message: message ]]; }; YggTransactionMap.AbortUnilaterally[l.first.waiter, timeout]; Process.Yield[]; FREE[@l]; l _ lNext; ENDLOOP; }; Process.Yield[]; DO l: LIST OF ConflictingTranses _ GetBlockingTransactions[]; currentTime: BasicTime.GMT _ BasicTime.Now[]; noAbortDone: BOOL _ TRUE; UNTIL l = NIL DO lNext: LIST OF ConflictingTranses _ l.rest; elapsedTimeSinceLastStartWork: INT _ BasicTime.Period[from: YggTransactionMap.GetTimeOfLastStartWork[l.first.holder], to: currentTime]; IF elapsedTimeSinceLastStartWork > abortInactiveGrantedRequestInterval THEN { logProc: PROC [YggMonitoringLog.TransactionAbortInfo]; fileName: Rope.ROPE; -- name of file being fought over IF (logProc _ YggMonitoringLog.notice.abortTransaction) # NIL THEN { fileName _ YggDIDMap.GetName[YggDIDMap.DocumentFromLockID[l.first.lock]]; logProc[[ transID: YggMonitoringHooks.TransIDFromTransHandle[l.first.holder], where: "LockWatchdogImpl.LockWatchdogProcess", why: watchDog, message: Rope.Cat[ "trans is holding up ", YggMonitoringHooks.TransIDToRope[YggMonitoringHooks.TransIDFromTransHandle[l.first.waiter]], "; waiting for ", IF fileName = NIL THEN "(NIL)" ELSE fileName ] ]] }; YggTransactionMap.AbortUnilaterally[l.first.holder, blockingNewLockRequest]; Process.Yield[]; noAbortDone _ FALSE; }; FREE[@l]; l _ lNext; ENDLOOP; IF noAbortDone THEN EXIT; ENDLOOP; Process.Yield[]; { g: WaitingForGraph; [numWaits, g] _ ComputeWaitingForGraph[numWaits]; IF g # NIL THEN { logProc: PROC [YggMonitoringLog.TransactionAbortInfo]; -- (used to stop race condition) DO t: YggTransactionMap.TransHandle; -- transaction to kill p _ FindCycle[g, p]; IF (t _ ChooseVictim[p]) = NIL THEN GOTO freeGraph; IF (logProc _ YggMonitoringLog.notice.abortTransaction) # NIL THEN { deadlockList: Rope.ROPE _ NIL; FOR i: NAT IN [0 .. p.length) DO deadlockList _ deadlockList.Cat[ " ", YggMonitoringHooks.TransIDToRope[YggMonitoringHooks.TransIDFromTransHandle[p[i].waitingTrans.trans]] ]; deadlockList _ deadlockList.Cat[ "(", YggDIDMap.GetName[YggDIDMap.DocumentFromLockID[p[i].waitingTrans.waitingFor]], ")" ]; ENDLOOP; logProc[[ transID: YggMonitoringHooks.TransIDFromTransHandle[t], where: "LockWatchdogImpl.LockWatchdogProcess", why: watchDog, message: Rope.Concat["deadlock of", deadlockList] ]]; }; YggTransactionMap.AbortUnilaterally[t, deadlock]; Process.Yield[]; g _ EliminateTrans[g, t]; REPEAT freeGraph => FreeGraph[g]; ENDLOOP; }; }; ENDLOOP; EXITS aborted => NULL }; WaitMemb: PROC [trans: YggTransactionMap.TransHandle, list: LIST OF ConflictingTranses] RETURNS [BOOLEAN] ~ { UNTIL list = NIL DO IF trans = list.first.waiter THEN RETURN [TRUE]; list _ list.rest ENDLOOP; RETURN [FALSE] }; BlockMemb: PROC [trans: YggTransactionMap.TransHandle, list: LIST OF ConflictingTranses] RETURNS [BOOLEAN] ~ { UNTIL list = NIL DO IF trans = list.first.holder THEN RETURN [TRUE]; list _ list.rest ENDLOOP; RETURN [FALSE] }; ChooseTimeoutVictims: PROC [wakeupPeriod: INT--msec--, abortWaitingRequestInterval: INT--seconds--] RETURNS [wakeupPeriodsUntilTimeoutCheck: INT, l: LIST OF ConflictingTranses] = { currentTime: BasicTime.GMT = BasicTime.Now[]; secondsToNextWakeup: INT _ abortWaitingRequestInterval; NoticeWaitingRequest: PROC [wr: WaitingRequest] RETURNS [stop: BOOL] = { m: LockMode = ModeAfterGrantingRequest[wr]; elapsedTimeSinceWait: INT--seconds-- = BasicTime.Period[from: wr.startTime, to: currentTime]; IF elapsedTimeSinceWait >= abortWaitingRequestInterval THEN { IF NOT WaitMemb[wr.trans, l] THEN { FOR h: Lock _ wr.requestList, h.requestList UNTIL h=wr DO WITH h SELECT FROM grh: GrantedRequest => IF grh.trans # wr.trans AND NOT YggLock.Compat[m][grh.mode] THEN { l _ CONS[first: [holder: grh.trans, waiter: wr.trans, lock: YggLockInternal.LockIDFromRH[wr]], rest: l]; GOTO FoundABlocker } ENDCASE; REPEAT FoundABlocker => NULL; FINISHED => l _ CONS[first: [holder: NIL, waiter: wr.trans, lock: YggLockInternal.LockIDFromRH[wr]], rest: l]; ENDLOOP; } } ELSE { secondsToNextWakeup _ MIN[secondsToNextWakeup, abortWaitingRequestInterval-elapsedTimeSinceWait]; }; RETURN [stop: FALSE]; }; -- NoticeWaitingRequest l _ NIL; YggLockInternal.GetInfo[waitingRequestEnumProc: NoticeWaitingRequest]; RETURN [(secondsToNextWakeup*1000 + wakeupPeriod-1)/wakeupPeriod, l]; }; GetBlockingTransactions: PROC [] RETURNS [l: LIST OF ConflictingTranses] = { NoticeWaitingRequest: PROC [wr: WaitingRequest] RETURNS [stop: BOOL] = { m: LockMode = ModeAfterGrantingRequest[wr]; FOR h: Lock _ wr.requestList, h.requestList UNTIL h=wr DO WITH h SELECT FROM grh: GrantedRequest => IF grh.trans # wr.trans AND NOT YggLock.Compat[m][grh.mode] AND NOT BlockMemb[grh.trans, l] THEN l _ CONS[first: [holder: grh.trans, waiter: wr.trans, lock: YggLockInternal.LockIDFromRH[wr]], rest: l]; ENDCASE; ENDLOOP; RETURN [stop: FALSE]; }; l _ NIL; YggLockInternal.GetInfo[waitingRequestEnumProc: NoticeWaitingRequest]; RETURN [l]; }; ModeAfterGrantingRequest: PROC [wr: WaitingRequest] RETURNS [LockMode] = { FOR h: Lock _ wr.requestList, h.requestList UNTIL h=wr DO WITH h SELECT FROM grh: GrantedRequest => IF grh.trans = wr.trans THEN RETURN[YggLock.Sup[wr.mode][grh.mode]]; ENDCASE; ENDLOOP; RETURN [wr.mode]; }; WaitingTransHandle: TYPE = REF WaitingTransObject; WaitingTransObject: TYPE = RECORD [ trans: YggTransactionMap.TransHandle, waitingFor: LockID _ , edges: LIST OF--WaitingTransHandle--REF ANY, visited: BOOL _ FALSE, onPath: BOOL _ FALSE ]; WaitingForGraph: TYPE = LIST OF--WaitingTransHandle--REF ANY; ComputeWaitingForGraph: PROC [numWaits: INT] RETURNS [newNumWaits: INT, g: WaitingForGraph] = { NoticeGeneralInfo: YggLockInternal.GeneralInfoProc = { newNumWaits _ nSetCallsWaited; }; NoticeWaitingRequest: PROC [wr: WaitingRequest] RETURNS [stop: BOOL] = { IF newNumWaits = numWaits THEN RETURN [stop: TRUE] ELSE { IF (LookupTrans[g, wr.trans] = NIL) THEN { wt: WaitingTransHandle _ NEW[WaitingTransObject _ [trans: wr.trans, edges: NIL, waitingFor: YggLockInternal.LockIDFromRH[wr]]]; g _ CONS[first: wt, rest: g]; }; RETURN [stop: FALSE]; }; }; NoticeWaitingRequest2: PROC [wr: WaitingRequest] RETURNS [stop: BOOL] = { IF newNumWaits = numWaits THEN RETURN [stop: TRUE] ELSE { waiting: WaitingTransHandle = LookupTrans[g, wr.trans]; m: LockMode = ModeAfterGrantingRequest[wr]; FOR h: Lock _ wr.requestList, h.requestList UNTIL h = wr DO WITH h SELECT FROM grh: GrantedRequest => IF grh.trans # wr.trans AND NOT YggLock.Compat[m][grh.mode] THEN { granted: WaitingTransHandle _ LookupTrans[g, grh.trans]; IF granted # NIL AND NOT List.Memb[granted, waiting.edges] THEN waiting.edges _ CONS[first: granted, rest: waiting.edges]; }; ENDCASE; ENDLOOP; RETURN [stop: FALSE]; }; }; g _ NIL; YggLockInternal.GetInfo[ generalInfoProc: NoticeGeneralInfo, waitingRequestEnumProc: NoticeWaitingRequest, waitingRequestEnumProc2: NoticeWaitingRequest2]; RETURN [newNumWaits, g]; }; PathRecord: TYPE = RECORD [ length: NAT _ 0, vertices: SEQUENCE maxLength: NAT OF RECORD [ waitingTrans: WaitingTransHandle, nextEdge: LIST OF--WaitingTransHandle--REF ANY ] ]; Path: TYPE = REF PathRecord; FindCycle: PROC [g: WaitingForGraph, oldPath: Path] RETURNS [path: Path] = { top: LIST OF REF ANY _ g; pathLast: NAT _ 0; v: WaitingTransHandle; pathMaxLength: NAT = List.Length[g]; path _ IF oldPath # NIL AND oldPath.length >= pathMaxLength THEN oldPath ELSE NEW[PathRecord[pathMaxLength]]; DO { -- EXITS NextTopLevelVertex v _ NARROW[top.first]; IF v.visited THEN GOTO NextTopLevelVertex; DO path[pathLast] _ [waitingTrans: v, nextEdge: v.edges]; v.visited _ v.onPath _ TRUE; DO e: LIST OF REF ANY _ path[pathLast].nextEdge; IF e = NIL THEN { path[pathLast].waitingTrans.onPath _ FALSE; IF pathLast = 0 THEN GOTO NextTopLevelVertex; pathLast _ pathLast - 1; } ELSE { path[pathLast].nextEdge _ e.rest; v _ NARROW[e.first]; IF v.onPath THEN GOTO FoundCycle; IF NOT v.visited THEN { pathLast _ pathLast + 1; EXIT; }; }; ENDLOOP; ENDLOOP; EXITS NextTopLevelVertex => IF (top _ top.rest) = NIL THEN { path.length _ 0; GOTO done; }; FoundCycle => { cycleStart: NAT; FOR cycleStart _ 0, cycleStart+1 UNTIL path[cycleStart].waitingTrans = v DO ENDLOOP; FOR j: NAT IN [cycleStart .. pathLast] DO path[j-cycleStart] _ [path[j].waitingTrans, NIL]; ENDLOOP; path.length _ pathLast-cycleStart+1; GOTO done; }; }; ENDLOOP; EXITS done => FOR j: NAT IN [path.length .. path.maxLength) DO path[j] _ [NIL, NIL]; ENDLOOP; }; ChooseVictim: PROC [p: Path] RETURNS [YggTransactionMap.TransHandle] = { minUpdateCost: INT _ LAST[INT]; minUpdateCostTrans: YggTransactionMap.TransHandle _ NIL; FOR i: NAT IN [0 .. p.length) DO updateCost: INT _ YggTransactionMap.GetEstimatedUpdateCost[p[i].waitingTrans.trans]; IF updateCost < minUpdateCost OR minUpdateCostTrans = NIL THEN { minUpdateCost _ updateCost; minUpdateCostTrans _ p[i].waitingTrans.trans; }; ENDLOOP; RETURN [minUpdateCostTrans]; }; LookupTrans: PROC [g: WaitingForGraph, t: YggTransactionMap.TransHandle] RETURNS [wt: WaitingTransHandle] = { FOR l: LIST OF REF ANY _ g, l.rest UNTIL l = NIL DO wt: WaitingTransHandle _ NARROW[l.first]; IF wt.trans = t THEN RETURN [wt]; ENDLOOP; RETURN [NIL]; }; EliminateTrans: PROC [g: WaitingForGraph, t: YggTransactionMap.TransHandle] RETURNS [newG: WaitingForGraph]= { wt: WaitingTransHandle _ LookupTrans[g, t]; FreeList[wt.edges]; wt.edges _ NIL; FOR l: LIST OF REF ANY _ g, l.rest UNTIL l = NIL DO w: WaitingTransHandle _ NARROW[l.first]; w.visited _ w.onPath _ FALSE; w.edges _ List.DRemove[wt, w.edges]; ENDLOOP; RETURN [List.DRemove[wt, g]]; }; FreeGraph: PROC [g: WaitingForGraph] = { FOR l: LIST OF REF ANY _ g, l.rest UNTIL l = NIL DO wt: WaitingTransHandle _ NARROW[l.first]; FreeList[wt.edges]; FREE[@wt]; ENDLOOP; FreeList[g]; }; FreeList: PROC [l: LIST OF REF ANY] = { UNTIL l = NIL DO lNext: LIST OF REF ANY _ l.rest; FREE[@l]; l _ lNext; ENDLOOP; }; END.--LockWatchdogImpl CHANGE LOG Changed by MBrown on February 6, 1983 10:03 pm ®YggLockWatchdogImpl.mesa Copyright Ó 1984, 1985, 1988 by Xerox Corporation. All rights reserved. MBrown on January 30, 1984 5:41:50 pm PST Kupfer, February 28, 1985 2:16:45 pm PST Hauser, May 20, 1985 1:40:32 pm PDT Bob Hagmann July 13, 1988 10:52:40 am PDT NOTES Some care has been taken to make these procedures efficient in the (normal) case that waiting happens rarely, the total number of waiting requests is small, and SkiPatrol logging is turned off for abortTransaction. This allows us to check more frequently, which improves response in cases when the wait can be resolved right away (deadlock and inactive blocking transaction). YggInternal.LockTransHeaderObject YggLockControl.ForkWatchdogProcess. For each waiting request, abort its transaction if it has waited for more than abortWaitingRequestInterval seconds. (Of course, don't bother to check for timed-out requests unless a request might be timed out, based on information obtained on the last check.) Collect a list of transactions that hold locks that other transactions need. For each blocking transaction: if it has done no work in the last abortInactiveGrantedRequestInterval seconds, abort it. Repeat until no transaction is aborted by this test. Compute a graph whose vertices are transactions with one or more waiting lock requests, containing a directed edge from t1 to t2 if t2 holds a lock that prevents a waiting lock request or t1 from being satisfied. (Don't bother to compute the graph if no lock requests have waited since the last time the graph was computed.) If the graph contains a cycle, find a cycle and abort the lowest-cost transaction in the cycle. Repeat until the graph contains no cycle. Log the transaction ID's of everyone involved in the deadlock. Support for timeout and blockingNewLockRequest detection. Tells whether "trans" appears as a "waiter" in "list". Tells whether "trans" appears as a "holder" in "list". INTERNAL to LockCoreImpl If "wr" is old and not already in our list of things to kill (l), then add it to the list. When we add it, we'd like to record a transaction ID for a transaction that is blocking it (there may be more than one). If we can't find a holder, record NIL as the holder (but kill the waiter anyway). wr is old and not already in the list, so find a holder wr isn't old; update time for next wakeup, if necessary INTERNAL to LockCoreImpl Look for transactions that are blocking wr. As one is found, add it to the list if it isn't already in it. INTERNAL to LockCoreImpl Support for deadlock detection. If numWaits equals the number of lock waits reported by the lock manager, return newNumWaits = numWaits, g = NIL. Otherwise compute the current waiting-for graph, and return it as g. If there are new waiters, add this transaction to the list of waiting transactions. If there are new waiters, create an edge for each transaction blocking this request. If g contains a cycle, returns a cycle as path; returns path.length = 0 if g contains no cycle. Reuses oldPath's storage if it can. Add the new vertex v to the end of the path. Explore the next edge out of the vertex at the end of the path. Remove a vertex from the end of the path. Add v to the end of the path. Move vertices of cycle down to start of sequence, clean up rest, and return. Transactions in p are deadlocked. Choose one to be aborted. Return NIL iff p is empty. Compute a new graph, eliminating t. Mark all vertices unvisited and not on path. NILs things out (superstition). Bug in GetBlockingTransactions: always returned the waiting transaction instead of the blocking transaction. This meant wait => abort nearly every time. Edited on July 24, 1984 11:01:18 am PDT, by Kupfer Add YggMonitoringLog probes. changes to: LockWatchdogImpl, LockWatchdogProcess Edited on July 29, 1984 7:08:42 pm PDT, by Kupfer Add code so that the lockConflict probes can report who the interested parties were. changes to: LockWatchdogImpl, ChooseTimeoutVictims, NoticeWaitingRequest (local of ChooseTimeoutVictims) Edited on August 6, 1984 3:31:53 pm PDT, by Kupfer Remove the possible race condition in YggMonitoringLog probes by assigning the PROC to a temporary variable. changes to: LockWatchdogImpl Edited on February 28, 1985 11:35:48 am PST, by Kupfer Record the name of the files being fought over when the watchdog has to kill some transaction. changes to: LockWatchdogImpl, NoticeWaitingRequest (local of ChooseTimeoutVictims), NoticeWaitingRequest (local of GetBlockingTransactions), ComputeWaitingForGraph ÊÕ˜Icodešœ™šœH™HKšœ)™)K™(K™#K™)—K™K˜ K˜Kšœ™K˜Kšœø™øK˜šÏk ˜ Kšœ˜Kšœ ˜ K˜ Kšœ œ˜.K˜Kšœ˜Kšœ˜Kšœ˜Kšœ˜Kšœœœ˜Kšœœ)˜AKšœœ ˜6Kšœ˜K˜—šœ ˜"š˜Kšœ|˜|—š˜Kšœ˜K˜—Kšœ˜K˜Kšœœ˜Kšœ(˜(Kšœ œ˜"Kšœœ˜4Kšœœ˜"Kšœ œ˜(Kšœœ˜&Kšœ œ˜(Kšœœ"˜6Kšœœ"˜6Kšœœ#˜8Kšœœœ^Ïc,˜«K˜šœœœ/˜QKšœ!™!K˜—K˜šÏnœœœ˜"Kšœ#˜#Kšœž œ˜,Kšœ%ž œ˜8Kšœ#™#Kšœœi˜…K˜K˜—šŸœœ˜Kšœ#˜#Kšœž œ˜,Kšœ%ž œ˜8Kšœ œ˜(Kšœ œ˜Kšœ œ˜š˜Kšœ2œœ ˜KK˜Kšœ„™„šœJœ˜RKšœœœ˜Kšœ œ+ž ˜XK˜K˜fšœœ˜Kšœœœ˜+šœ8œœ˜DKšœœž%˜;Kšœœ˜šœ œœ˜&˜2Kšœ\˜\——š˜K˜#—KšœI˜I˜K˜šœ œ˜Kšœ˜—š˜Kšœ˜—K˜—šœ ˜ KšœC˜CKšœ.˜.Kšœ˜Kšœ˜K˜—K˜—Kšœ=˜=Kšœ˜Kšœ˜ K˜ Kšœ˜—K˜K˜—KšœÏr#œI™üKšœ˜š˜Kšœœœ0˜:Kšœœ˜-Kšœ œœ˜šœœ˜Kšœœœ˜+Kšœœe˜‡šœEœ˜MKšœ œ)˜6Kšœœž!˜7šœ8œ˜DKšœI˜Išœ ˜ KšœC˜CKšœ.˜.Kšœ˜˜K˜Kšœ\˜\Kšœ˜šœœ˜K˜—šœ˜K˜—K˜—K˜—K˜—KšœL˜LKšœ˜Kšœœ˜K˜—Kšœ˜ K˜ Kšœ˜—Kšœ œœ˜Kšœ˜K˜—KšœÑ™ÑKšœ˜˜K˜K˜1šœœœ˜Kšœ œ+ž ˜Xš˜Kšœ"ž˜8K˜Kšœœœœ ˜3šœ8œ˜DKšœ>™>Kšœœœ˜šœœœ˜ ˜ K˜Kšœd˜dKšœ˜—˜ K˜KšœN˜NK˜K˜—Kšœ˜—šœ ˜ Kšœ6˜6Kšœ.˜.Kšœ˜K˜1K˜—K˜—Kšœ1˜1Kšœ˜K˜š˜K˜—Kšœ˜—K˜—K˜K˜—Kšœ˜—š˜Kšœ ˜—K˜—K˜—˜Iheadšœ9™9K˜š Ÿœœ.œœœœ˜mJšœ6™6šœœ˜Jšœœœœ˜0J˜Jšœ˜—Jšœœ˜J˜J˜—š Ÿ œœ.œœœœ˜nJšœ6™6šœœ˜Jšœœœœ˜0J˜Jšœ˜—Jšœœ˜J˜J˜—K˜šŸœœžœž œœ"œœœ˜´Kšœœ˜-Kšœœ˜7unitšŸœœœœ˜HKšœ™K˜+Kšœž œ9˜]Kšœ> œè™§šœ5œ˜?šœœœ˜#Jš œ5™7šœ)œ˜9šœœ˜šœ˜šœœœœ˜BKšœœ`˜hKšœ˜K˜——Kšœ˜—š˜Kšœœ˜šœ˜ KšœœœF˜b——Kšœ˜—K˜—K˜—šœ˜Kš œ5™7KšœœH˜aKšœ˜—Kšœœ˜Kšœž˜K˜—Kšœœ˜KšœF˜FKšœ?˜EK˜K˜K˜K˜—š Ÿœœœœœ˜LšŸœœœœ˜HKšœ™K˜+Jšœ( œA™kšœ)œ˜9šœœ˜šœ˜š œœœœœ˜`Kšœœ`˜h——Kšœ˜—Kšœ˜—Kšœœ˜K˜K˜—Kšœœ˜KšœF˜FKšœ˜ K˜K˜—šŸœœœ˜JKšœ™šœ)œ˜9šœœ˜šœ˜Kšœœœ!˜D—Kšœ˜—Kšœ˜—Kšœ ˜K˜—K˜—˜Lšœ™K˜Kšœœœ˜2šœœœ˜#Kšœ%˜%K˜Kš œœžœœ˜,Kšœ œœ˜Kšœœ˜K˜—Kš œœœžœœ˜=K˜šŸœœ œ˜,Kšœœ˜2Kšœ·™·šŸœ%˜6K˜K˜—šŸœœœœ˜HKšœS™SKšœœœœ˜2šœ˜šœœœ˜*Kšœœ/œ1˜Kšœœ˜K˜—Kšœœ˜K˜—K˜—šŸœœœœ˜IKšœT™TKšœœœœ˜2šœ˜K˜7K˜+šœ)œ˜;šœœ˜šœ˜šœœœœ˜BK˜8š œ œœœ#˜?Kšœœ&˜:—K˜——Kšœ˜—Kšœ˜—Kšœœ˜K˜—K˜—Kšœœ˜šœ˜K˜#K˜-K˜0—Kšœ˜K˜——˜šœ œœ˜Kšœœ˜š œ œ œœœ˜-K˜!Kšœ œžœ˜.K˜—K˜—Kšœœœ ˜K˜šŸ œœ%œ˜LKšœ„™„Kš œœœœœ˜Kšœ œ˜K˜Kšœœ˜$šœœ œœ ˜;Kšœ˜ Kšœœ˜$—š˜šœž˜Kšœœ ˜Kšœ œœ˜*š˜Kšœ,™,K˜6Kšœœ˜š˜Kšœ?™?Kš œœœœœ˜-šœœœ˜Kšœ)™)Kšœ%œ˜+Kšœœœ˜-K˜K˜—šœ˜K˜!Kšœœ ˜Kšœ œœ ˜!šœœ œ˜Kšœ™K˜Kšœ˜K˜—K˜—Kšœ˜—Kšœ˜—š˜šœœœœ˜6K˜Kšœ˜ K˜—˜KšœL™LKšœ œ˜šœ˜ Kšœ#œœ˜3—šœœœ˜)Kšœ,œ˜1Kšœ˜—K˜$Kšœ˜ K˜——K˜—Kšœ˜—šœ˜ šœœœ!˜0Kšœ œœ˜Kšœ˜——K˜——˜šŸ œœ œ$˜HKšœ<™