-- File: VMCacheMgr.mesa
-- Last edited by Levin:  12-Apr-83 11:28:46

DIRECTORY
  LogDefs USING [DisplayNumber],
  ProcessDefs USING [MsecToTicks, Yield],
  VMDefs USING [CantReadBackingStore, CantWriteBackingStore, Error],
  VMPrivate USING [
    AcquirePage, AddressToMDSPage, CacheIndex, FileHandle, FileObject, HandleTable,
    IndexToHandle, loggingEnabled, MDSPageToAddress, nilCacheIndex, nilMDSPage,
    Object, PageHandle, PageObject, RemovePageFromTables, WriteEnable,
    WritePageToFS, WriteProtect],
  VMSpecial USING [],
  VMStorage USING [AllocatePage, FreePage, longTerm];

VMCacheMgr: MONITOR LOCKS lock↑
  IMPORTS LogDefs, ProcessDefs, VMDefs, VMPrivate, VMStorage
  EXPORTS VMDefs, VMPrivate, VMSpecial =

  BEGIN OPEN VMPrivate;


  -- The VMCache manager synchronizes access to the cache data structures.  However,
  -- there is a subtlety involving the PruneCache procedure, which is invoked when
  -- storage is low.  This procedure may be invoked at essentially arbitrary times and
  -- must be able to acquire the cache.  To prevent deadlock, storage allocation
  -- requests generated by VMCacheOps must either be made outside the cache mutual
  -- exclusion or must temporarily disable the PruneCache.  Examples occur in
  -- EnsureBufferExists, which calls AllocatePage, and FillCacheSlot, which calls
  -- the node-level allocator.


  -- Types and Related Constants --

  CacheState: TYPE = {stable, unstable};

  VictimClass: TYPE = {
    available, nil, cleanOver, dirtyOver, cleanUnder, dirtyUnder, none};


  -- Global Variables --

  lock: POINTER TO MONITORLOCK;
  LOCK: MONITORLOCK;
  
  cacheState: CacheState;
  cacheStable: CONDITION;

  minCachePages, maxCachePages: CacheIndex;

  handles: POINTER TO HandleTable;

  buffersInUse: CacheIndex;

  rover: CacheIndex;

  flushDisabled: BOOLEAN;


  -- Miscellaneous Declarations --

  hopeForStateChange: CONDITION;

  PageInUse: ERROR = CODE;


  -- Types Exported to VMDefs --

  FileObject: PUBLIC TYPE = VMPrivate.FileObject;


  -- Procedures and Signals Exported to VMSpecial --

  PruneCache: PUBLIC PROCEDURE [pages: CARDINAL] RETURNS [BOOLEAN] =
    BEGIN

    StealOneBuffer: PROCEDURE =
      -- releases a single page buffer.
      BEGIN
      victim: CacheIndex;
      page: PageHandle;
      [victim, ] ← LiberateCacheSlot[bufferRequired: TRUE];
      page ← IndexToHandle[victim];
      WriteEnable[page.buffer];
      VMStorage.FreePage[MDSPageToAddress[page.buffer]];
      page.buffer ← nilMDSPage;
      buffersInUse ← buffersInUse - 1;
      END;

    IF flushDisabled OR buffersInUse <= minCachePages THEN RETURN[FALSE];
    AcquireCache[];
    IF pages = 1 THEN StealOneBuffer[]
    ELSE UNTIL buffersInUse <= minCachePages DO StealOneBuffer[]; ENDLOOP;
    ReleaseCache[];
    RETURN[TRUE]
    END;


  -- Procedures and Signals Exported to VMPrivate --

  InitializeVMCacheMgr: PUBLIC PROCEDURE [
    h: POINTER TO HandleTable, min, max: CacheIndex] =
    BEGIN
    lock ← @LOCK;
    handles ← h;
    minCachePages ← min;
    maxCachePages ← max;
    rover ← FIRST[CacheIndex];
    hopeForStateChange.timeout ← ProcessDefs.MsecToTicks[100];
    flushDisabled ← FALSE;
    buffersInUse ← 0;
    cacheState ← stable;
    cacheStable.timeout ← 0;
    IF loggingEnabled THEN
      LogDefs.DisplayNumber["VM Cache Pages"L, [short[@buffersInUse]]];
    END;

  FinalizeVMCacheMgr: PUBLIC PROCEDURE =
    BEGIN
    FOR index: CacheIndex IN [0..handles.nHandles) DO
      page: PageHandle ← handles[index];
      IF page ~= NIL THEN
	BEGIN
	IF ~(page.useCount = 0 AND page.file = NIL) THEN ERROR PageInUse;
	IF page.buffer ~= nilMDSPage THEN
	  BEGIN
	  WriteEnable[page.buffer];
	  VMStorage.FreePage[MDSPageToAddress[page.buffer]];
	  END;
	VMStorage.longTerm.FREE[@page];
	END;
      ENDLOOP;
    END;

  -- Cache Mutual Exclusion --

  AcquireCache: PUBLIC ENTRY PROCEDURE =
    BEGIN
    UNTIL cacheState = stable DO WAIT cacheStable; ENDLOOP;
    cacheState ← unstable;
    END;

  ReleaseCache: PUBLIC ENTRY PROCEDURE =
    {cacheState ← stable; NOTIFY cacheStable};

  -- Cache Management --

  AllocateCacheIndex: PUBLIC PROCEDURE RETURNS [CacheIndex] =
    BEGIN
    page: PageHandle;
    victim: CacheIndex;
    vc: VictimClass;
    AcquireCache[];
    [victim, vc] ← LiberateCacheSlot[bufferRequired: FALSE];
    IF vc = nil THEN FillCacheSlot[victim];
    page ← IndexToHandle[victim];
    page.state ← unstable;
    ReleaseCache[];
    EnsureBufferExists[victim];
    RETURN[victim]
    END;

  EnumerateCachePagesInFile: PUBLIC PROCEDURE [
    file: FileHandle,
    proc: PROCEDURE [page: PageHandle] RETURNS [found, unmap: BOOLEAN]]
    RETURNS [outcome: BOOLEAN] =
    BEGIN
    outcome ← FALSE;
    DO
      pagesBeingTaken: BOOLEAN ← FALSE;
      slot, last: CacheIndex;
      AcquireCache[];
      slot ← last ← rover;
      DO
	page: PageHandle = handles[slot];
	IF page ~= NIL AND page.file = file THEN
	  IF page.beingTaken OR page.state = unstable THEN pagesBeingTaken ← TRUE
	  ELSE
	    BEGIN
	    unmap: BOOLEAN;
	    page.useCount ← page.useCount + 1;
	    [found: outcome, unmap: unmap] ← proc[page ! UNWIND => ReleaseCache[]];
	    IF (page.useCount ← page.useCount - 1) = 0 THEN
	      IF unmap THEN RemovePageFromTables[page]
	      ELSE WriteProtect[page.buffer];
	    IF outcome THEN {ReleaseCache[]; RETURN};
	    END;
	slot ← IF slot = maxCachePages - 1 THEN FIRST[CacheIndex] ELSE slot + 1;
	IF slot = last THEN EXIT;
	ENDLOOP;
      ReleaseCache[];
      IF ~pagesBeingTaken THEN EXIT;
      ProcessDefs.Yield[];
      ENDLOOP;
    END;


  -- Internal Procedures --

  -- Cache Management --

  EnumerateCacheSlots: PROCEDURE [
    proc: PROCEDURE [slot: CacheIndex] RETURNS [BOOLEAN]] RETURNS [BOOLEAN] =
    BEGIN
    slot: CacheIndex ← rover;
    last: CacheIndex ← rover;
    DO
      IF proc[slot] THEN RETURN[TRUE];
      slot ← IF slot = maxCachePages - 1 THEN FIRST[CacheIndex] ELSE slot + 1;
      IF slot = last THEN RETURN[FALSE];
      ENDLOOP;
    END;

  LiberateCacheSlot: PROCEDURE [bufferRequired: BOOLEAN]
    RETURNS [victim: CacheIndex, vc: VictimClass] =
    -- uses FindPotentialVictim to locate a candidate, writes the victim out to disk
    -- if dirty, and, if necessary, removes it from the mapping tables.
    BEGIN
    DO
      page: PageHandle;
      [victim, vc] ← FindPotentialVictim[bufferRequired];
      SELECT vc FROM nil, available => EXIT; ENDCASE;
      page ← IndexToHandle[victim];
      IF page.dirty THEN
	BEGIN OPEN VMDefs;
	AcquirePage[page];
	page.beingTaken ← TRUE; -- ensure page doesn't disappear
	ReleaseCache[];
	WritePageToFS[page: page, wait: TRUE !
	  CantReadBackingStore, CantWriteBackingStore, Error => CONTINUE];
	AcquireCache[];
	page.beingTaken ← FALSE;
	IF page.dirty OR page.useCount > 0 THEN LOOP;
	END;
      RemovePageFromTables[page];
      EXIT;
      ENDLOOP;
    END;

  FindPotentialVictim: PROCEDURE [bufferRequired: BOOLEAN]
    RETURNS [victim: CacheIndex, vc: VictimClass] = -- INLINE --
    -- locates a candidate victim slot in the cache, using the replacement algorithm.
    -- For normal replacement purposes, 'bufferRequired' should be FALSE.  However, if
    -- the selected victim must have a buffer already allocated (e.g., as when returning
    -- buffer space to the Mesa runtime), then 'bufferRequired' should be TRUE.
    BEGIN

    CheckSlot: PROCEDURE [slot: CacheIndex] RETURNS [stopLooking: BOOLEAN] =
      -- classifies and potentially selects as a possible victim the page in cache
      -- slot 'slot'.  Returns TRUE when a "first-rate" victim has been found.
      BEGIN
      page: PageHandle = handles[slot]; -- don't use IndexToHandle, since NIL is OK.
      file: FileHandle;
      newvc: VictimClass;
      stopLooking ← FALSE;
      BEGIN -- inner block permits 'MightBeDone' to access 'page' and 'newvc'
      IF page = NIL THEN {newvc ← nil; GO TO MightBeDone};
      IF page.useCount ~= 0 OR page.state = unstable OR page.beingTaken THEN RETURN;
      IF page.file = NIL THEN {newvc ← available; GO TO MightBeDone};
      IF page.age = new THEN RETURN;
      file ← page.file;
      newvc ←
	IF page.dirty THEN
	IF file.useCount <= file.cachePages THEN dirtyUnder ELSE dirtyOver
	ELSE IF file.useCount <= file.cachePages THEN cleanUnder ELSE cleanOver;
      IF newvc < vc THEN {vc ← newvc; victim ← slot};
      IF vc = cleanOver THEN stopLooking ← TRUE;
      EXITS
	MightBeDone =>
	  BEGIN -- newvc is either nil or available
	  IF bufferRequired AND
	    (newvc = nil OR (newvc = available AND page.buffer = nilMDSPage)) THEN
	    RETURN;
	  victim ← slot;
	  vc ← newvc;
	  stopLooking ← TRUE;
	  END;
      END;
      END;

    AgeSlot: PROCEDURE [slot: CacheIndex] RETURNS [BOOLEAN] =
      -- marks the indicated cache slot 'old', if it is legitimate to do so.  Note
      -- that we need not test for page.beingTaken, since such a page is already
      -- marked old.
      BEGIN
      page: PageHandle = handles[slot];  -- don't use IndexToHandle, since NIL is OK.
      IF page ~= NIL AND page.useCount = 0 AND page.file ~= NIL AND
	page.state = stable THEN page.age ← old;
      RETURN[FALSE]
      END;

    WaitForBetterTimes: ENTRY PROCEDURE = INLINE
      -- in desperation, we delay for a while.
      {WAIT hopeForStateChange};

    DO
      victim ← nilCacheIndex;
      vc ← none;
      [] ← EnumerateCacheSlots[CheckSlot];
      SELECT vc FROM
	available, nil => EXIT;
	none => -- no old, stealable pages are present in the cache
	  BEGIN
	  [] ← EnumerateCacheSlots[AgeSlot]; -- age everything
	  [] ← EnumerateCacheSlots[CheckSlot];
	  IF vc ~= none THEN EXIT;
	  -- things are very grim...
	  ReleaseCache[];
	  WaitForBetterTimes[];
	  AcquireCache[];
	  END;
	ENDCASE => -- cleanOver, cleanUnder, dirtyOver, dirtyUnder
	  BEGIN
	  -- age all cache slots between 'rover' and 'victim', inclusive.
	  -- Note: we expand EnumerateCacheSlots inline for efficiency.  See notes
	  -- on AgeSlot, above.
	  slot: CacheIndex ← rover;
	  DO
	    page: PageHandle = handles[slot];
	    IF page ~= NIL AND page.useCount = 0 AND page.file ~= NIL AND
	      page.state = stable THEN page.age ← old;
	    IF slot = victim THEN EXIT;
	    slot ← IF slot = maxCachePages - 1 THEN FIRST[CacheIndex] ELSE slot + 1;
	    ENDLOOP;
	  EXIT
	  END;
      ENDLOOP;
    rover ← victim;
    END;

  FillCacheSlot: PROCEDURE [index: CacheIndex] = INLINE
    -- creates a page object and associated buffer, marks it 'available', and places
    -- it in cache slot 'index'.
    BEGIN
    page: PageHandle;
    flushDisabled ← TRUE; -- tough break, Mesa
    handles[index] ← page ← VMStorage.longTerm.NEW[PageObject ← Object[page[
      state: unstable, age: , dirty: FALSE, errorStatus: , buffer: nilMDSPage,
      pageStable: [timeout: 0], file: NIL, beingTaken: FALSE, page: , hashLink: ,
      recordNextVda: , useCount: 0]]];
    flushDisabled ← FALSE;
    END;

  EnsureBufferExists: PROCEDURE [index: CacheIndex] =
    -- ensures that the page in cache slot 'index' has an associated buffer.
    BEGIN
    page: PageHandle ← IndexToHandle[index];
    IF page.buffer = nilMDSPage THEN
      BEGIN
      page.buffer ← AddressToMDSPage[VMStorage.AllocatePage[]];
      buffersInUse ← buffersInUse + 1;
      END
    ELSE WriteEnable[page.buffer];
    END;

  END.