-- File: VMIO.mesa
-- Last edited by Levin:  12-Apr-83 12:06:34

DIRECTORY
  AltoFile USING [
    EnterDiskAddress, FileToAltoPageNumber, GetFileID, MapPageToDiskAddress,
    PageNumber],
  DiskIODefs USING [
    CompletionStatus, DiskRequest, fillInvDA, InitiateDiskIO, vDA,
    VerboseCompletionProcedure, XferSpec],
  FileDefs USING [Completer, FileHandle, PageNumber],
  Inline USING [LowHalf],
  LogDefs USING [DisplayNumber, Percentage],
  VMDefs USING [CantReadBackingStore, Error, LookAheadCount, Page, PageAddress,
    Position, Problem],
  VMPrivate USING [
    AcquireCache, AcquirePage, AllocateCacheIndex, CacheIndex, EnterInHashTable,
    EnterInPageTable, FileHandle, FileObject, IndexToHandle, LookupInHashTable,
    loggingEnabled, MDSPageNumber, MDSPageToAddress, nilCacheIndex, ObjectHandle,
    PageHandle, ReleaseCache, ReleasePage, ValidateFile, ValidatePageNumber,
    WaitUntilStable, WriteEnable];

VMIO: PROGRAM
  IMPORTS AltoFile, DiskIODefs, Inline, LogDefs, VMDefs, VMPrivate
  EXPORTS VMDefs, VMPrivate =

  BEGIN OPEN VMDefs, VMPrivate;


  -- Statistics Logging --

  readPageCalls, readPageCacheHits: LONG CARDINAL;

  cacheHitPercent: LogDefs.Percentage;


  -- Miscellaneous Declarations --

  maxMaxLookAhead: LookAheadCount = 11;  -- sectors/track - 1

  BadAddress: ERROR = CODE;


  -- Procedures, Signals, and Types Exported to VMDefs --

  FileObject: PUBLIC TYPE = VMPrivate.FileObject;

  ReadPage: PUBLIC PROCEDURE [addr: PageAddress, lookAhead: LookAheadCount ← 0]
    RETURNS [Page] =
    BEGIN
    page: PageHandle;

    FixUseCount: PROCEDURE = INLINE
      -- decrements page.useCount after an error.
      BEGIN
      AcquirePage[page]; -- needn't remap 'addr' because useCount > 0
      page.useCount ← page.useCount - 1;
      ReleasePage[page];
      END;

    ValidateAddressAndAcquireCache[addr];
    DoReads[addr, lookAhead, TRUE];
    ReleaseCache[];
    page ← IndexToHandle[LookupInHashTable[addr]]; -- Lookup can't fail; useCount > 0
    IF loggingEnabled THEN
      BEGIN -- approximately correct, since page.state may be changing
      readPageCalls ← readPageCalls + 1;
      IF page.state = stable THEN readPageCacheHits ← readPageCacheHits + 1;
      cacheHitPercent ← Inline.LowHalf[(readPageCacheHits*100)/readPageCalls];
      END;
    UNTIL WaitUntilStable[page, reading ! CantReadBackingStore => FixUseCount[]] = stable
      DO
      -- The read was never started, a situation that can only occur if the page in
      -- in question is on the local disk.  The page is in the hash and page tables and
      -- the use count is non-zero, but the buffer has invalid content and the page
      -- object is unstable.  If multiple readers attempt to access this page, mutual
      -- exclusion is assured by WaitUntilStable if it returns FALSE, since it leaves
      -- 'page' unstable.
      request: DiskIODefs.DiskRequest;
      xferSpec: ARRAY [0..1) OF DiskIODefs.XferSpec;
      vmFile: FileHandle = page.file;
      SetUpDiskRequest[@request, vmFile];
      PrepareXfer[@xferSpec[0], page];
      request.xfers ← DESCRIPTOR[@xferSpec, 1];
      IssueDiskRequest[@request, vmFile];
      ENDLOOP;
    RETURN[MDSPageToAddress[page.buffer]]
    END;

  StartReading: PUBLIC PROCEDURE [addr: PageAddress, lookAhead: LookAheadCount ← 0] =
    BEGIN
    ValidateAddressAndAcquireCache[addr];
    DoReads[addr, lookAhead, FALSE];
    ReleaseCache[];
    END;



  -- Procedures and Signals Exported to VMPrivate --

  maxLookAhead: PUBLIC LookAheadCount;

  -- Page I/O --

  WritePageToFS: PUBLIC PROCEDURE [page: PageHandle, wait: BOOLEAN] =
    BEGIN
    vmFile: FileHandle = page.file;
    fFile: FileDefs.FileHandle = vmFile.fh;
    DO
      -- loops only in the case that the write is never started (which can't
      -- happen if extending)
      extending: BOOLEAN;
      newLength: Position;
      [extending, newLength] ← ValidatePageAddress[page];
      page.errorStatus ← ok;
      IF extending THEN
	BEGIN
	vmFile.fs.ops.extend[fFile, newLength, MDSPageToAddress[page.buffer] !
	  VMDefs.Error => {page.errorStatus ← reason; CONTINUE}];
	IF page.errorStatus = ok THEN page.dirty ← FALSE;
	ReleasePage[page];
	END
      ELSE
	IF vmFile.altoFile THEN
	  BEGIN
	  nearPage: AltoFile.PageNumber;
	  nearvDA: DiskIODefs.vDA;
	  xferSpec: ARRAY [0..1) OF DiskIODefs.XferSpec;
	  knownPages: CARDINAL;
	  request: DiskIODefs.DiskRequest ←
	    [firstPage:, fileID: AltoFile.GetFileID[fFile], firstPagevDA:,
	      pagesToSkip:, nonXferID: vmFile, xfers: DESCRIPTOR[@xferSpec, 1],
	      proc: [verbose[AltoWriteComplete]], noRestore: FALSE, command: WriteD[]];
	  xferSpec[0] ← [MDSPageToAddress[page.buffer], DiskIODefs.fillInvDA, page];
	  [nearPage, nearvDA, knownPages] ←
	    AltoFile.MapPageToDiskAddress[fFile, page.page, 1];
	  request.firstPage ← AltoFile.FileToAltoPageNumber[nearPage];
	  request.firstPagevDA ← nearvDA;
	  IF (request.pagesToSkip ← page.page - nearPage) = 0 THEN
	    {xferSpec[0].diskAddress ← nearvDA; page.recordNextVda ← (knownPages = 1)};
	  DiskIODefs.InitiateDiskIO[@request];
	  END
	ELSE
	  vmFile.fs.ops.startWrite[fFile, page.page, MDSPageToAddress[page.buffer],
	    FSWriteComplete, page];
      IF ~wait OR WaitUntilStable[page, writing] = stable THEN EXIT;
      ENDLOOP;
    END;

  -- Start/Stop --

  InitializeVMIO: PUBLIC PROCEDURE =
    BEGIN
    IF loggingEnabled THEN
      BEGIN
      readPageCalls ← readPageCacheHits ← cacheHitPercent ← 0;
      LogDefs.DisplayNumber["VM Cache Hits"L, [percent[@cacheHitPercent]]];
      END;
    END;

  FinalizeVMIO: PUBLIC PROCEDURE =
    {NULL};

  -- Miscellaneous --

  ValidatePageAddress: PUBLIC PROCEDURE [page: PageHandle]
    RETURNS [extending: BOOLEAN, newLength: Position] =
    BEGIN
    vmFile: FileHandle = page.file;
    length: Position ← vmFile.fs.ops.getLength[vmFile.fh];
    SELECT TRUE FROM
      page.page > length.page => ERROR BadAddress;
      page.page = length.page AND length.byte = 0 => RETURN[TRUE, [page.page + 1, 0]];
      ENDCASE => RETURN[FALSE, length];
    END;



  -- Internal Procedures --

  -- Page Input --

  DoReads: PROCEDURE [addr: PageAddress, lookAhead: LookAheadCount, bump: BOOLEAN] =
    -- does the common part of ReadPage and StartReading.  If 'bump' is TRUE, the
    -- useCount for the page corresponding to 'addr' will be incremented (this prevents
    -- it from being stolen by any look-ahead).
    BEGIN OPEN DiskIODefs;
    vmFile: FileHandle = addr.file;
    fileLength: Position = vmFile.fs.ops.getLength[vmFile.fh];
    pg: FileDefs.PageNumber;
    actualReads: CARDINAL;
    xferSpecs: ARRAY [0..maxMaxLookAhead] OF XferSpec;
    slot: [0..maxMaxLookAhead];
    request: DiskRequest;

    FlushReads: PROCEDURE =
      -- initiates accumulated read requests.
      BEGIN
      IF vmFile.altoFile AND slot > 0 THEN
	BEGIN
	request.xfers ← DESCRIPTOR[@xferSpecs, slot];
	IssueDiskRequest[@request, vmFile];
	slot ← 0;
	END;
      END;

    IF (pg ← addr.page + (IF fileLength.byte = 0 THEN 1 ELSE 0)) > fileLength.page
      THEN ERROR BadAddress;
    actualReads ← MIN[lookAhead, fileLength.page - pg, maxLookAhead, maxMaxLookAhead] + 1;
    IF vmFile.altoFile THEN {slot ← 0; SetUpDiskRequest[@request, vmFile]};
    THROUGH [0..actualReads) DO
      page: PageHandle;
      oldindex, newindex: CacheIndex;
      BEGIN
      IF (oldindex ← LookupInHashTable[addr]) ~= nilCacheIndex THEN GO TO AlreadyIn;
      ReleaseCache[];
      newindex ← AllocateCacheIndex[];
      AcquireCache[];
      page ← IndexToHandle[newindex];
      IF (oldindex ← LookupInHashTable[addr]) ~= nilCacheIndex THEN
	{page.state ← stable; GO TO AlreadyIn};
      page.file ← addr.file; page.page ← addr.page;
      page.dirty ← FALSE;
      EnterInPageTable[page.buffer, newindex];
      EnterInHashTable[page];
      IF vmFile.altoFile THEN {PrepareXfer[@xferSpecs[slot], page]; slot ← slot + 1}
      ELSE
        vmFile.fs.ops.startRead[vmFile.fh, page.page, MDSPageToAddress[page.buffer],
	  FSReadComplete, page];
      EXITS AlreadyIn => {page ← IndexToHandle[oldindex]; FlushReads[]};
      END;
      page.age ← new;
      IF bump THEN
        BEGIN
	IF page.useCount = 0 THEN WriteEnable[page.buffer];
	page.useCount ← page.useCount + 1;
	bump ← FALSE;
	END;
      addr.page ← addr.page + 1;
      ENDLOOP;
    FlushReads[];
    END;

  SetUpDiskRequest: PROCEDURE [
    request: POINTER TO DiskIODefs.DiskRequest, vmFile: FileHandle] =
    -- initializes a disk request block for the given file.
    BEGIN
    request↑ ← DiskIODefs.DiskRequest[
      firstPage:, fileID: AltoFile.GetFileID[vmFile.fh], firstPagevDA:,
      pagesToSkip:, nonXferID: vmFile, xfers:, proc: [verbose[AltoReadComplete]],
      noRestore: FALSE, command: ReadD[]];
    END;

  PrepareXfer: PROCEDURE [xfer: POINTER TO DiskIODefs.XferSpec, page: PageHandle] =
    -- builds an XferSpec record for the given page and prepares the page object for
    -- the transfer.
    BEGIN OPEN DiskIODefs;
    page.errorStatus ← ok;
    page.recordNextVda ← FALSE;
    xfer↑ ← XferSpec[MDSPageToAddress[page.buffer], fillInvDA, page];
    END;

  IssueDiskRequest: PROCEDURE [
    request: POINTER TO DiskIODefs.DiskRequest, vmFile: FileHandle] =
    -- completes the disk request block and initiates the I/O.
    BEGIN OPEN AltoFile, DiskIODefs;
    nearPage: PageNumber;
    nearvDA: vDA;
    knownPages: CARDINAL;
    firstRead: PageNumber = LOOPHOLE[request.xfers[0].id, PageHandle].page;
    [nearPage, nearvDA, knownPages] ←
      MapPageToDiskAddress[vmFile.fh, firstRead, LENGTH[request.xfers]];
    request.firstPage ← FileToAltoPageNumber[nearPage];
    request.firstPagevDA ← nearvDA;
    IF (request.pagesToSkip ← firstRead - nearPage) = 0 THEN
      BEGIN
      request.xfers[0].diskAddress ← nearvDA;
      FOR i: CARDINAL IN [knownPages-1..LENGTH[request.xfers]) DO
	LOOPHOLE[request.xfers[i].id, PageHandle].recordNextVda ← TRUE;
	ENDLOOP;
      END;
    InitiateDiskIO[request];
    END;

  AltoReadComplete: DiskIODefs.VerboseCompletionProcedure =
    -- handles termination of local file read operations initiated by DoReads.
    BEGIN OPEN AltoFile;
    WITH obj: LOOPHOLE[id, ObjectHandle] SELECT FROM
      file => -- i.e., a page we passed over on the way
	IF status = ok THEN
	  EnterDiskAddress[obj.fh, label.page + 1, label.next ! ANY => CONTINUE];
      page => -- i.e., a page we read
	BEGIN
	IF (obj.errorStatus ← MapAltoStatusToProblem[status]) = ok THEN
	  IF obj.recordNextVda THEN
	    BEGIN
	    EnterDiskAddress[obj.file.fh, label.page+1, label.next ! ANY => CONTINUE];
	    obj.recordNextVda ← FALSE;
	    END;
	ReleasePage[@obj];
	END;
      ENDCASE;
    END;

  FSReadComplete: FileDefs.Completer =
    -- handles termination of remote file read operations initiated by DoReads.
    BEGIN
    page: PageHandle = LOOPHOLE[arg];
    page.errorStatus ← outcome;
    ReleasePage[page];
    END;

  -- Page Output --

  AltoWriteComplete: DiskIODefs.VerboseCompletionProcedure =
    -- handles termination of Alto file write operations initiated by WritePageToFS.
    BEGIN OPEN AltoFile;
    WITH obj: LOOPHOLE[id, ObjectHandle] SELECT FROM
      file => -- i.e., a page we passed over on the way
	IF status = ok THEN
	  EnterDiskAddress[obj.fh, label.page + 1, label.next ! ANY => CONTINUE];
      page => -- i.e., the page we wrote
	BEGIN
	IF (obj.errorStatus ← MapAltoStatusToProblem[status]) = ok THEN
	  BEGIN
	  obj.dirty ← FALSE;
	  IF obj.recordNextVda THEN
	    BEGIN
	    EnterDiskAddress[obj.file.fh, label.page+1, label.next ! ANY => CONTINUE];
	    obj.recordNextVda ← FALSE;
	    END;
	  END;
	ReleasePage[@obj];
	END;
      ENDCASE;
    END;

  FSWriteComplete: FileDefs.Completer =
    -- handles termination of remote file write operations initiated by WritePageToFS.
    BEGIN
    page: PageHandle = LOOPHOLE[arg];
    IF (page.errorStatus ← outcome) = ok THEN page.dirty ← FALSE;
    ReleasePage[page];
    END;

  MapAltoStatusToProblem: PROCEDURE [status: DiskIODefs.CompletionStatus]
    RETURNS [Problem] = INLINE
    {RETURN[SELECT status FROM ok => ok, neverStarted => other, ENDCASE => io]};

  -- Miscellaneous --

  ValidateAddressAndAcquireCache: PROCEDURE [addr: PageAddress] =
    BEGIN
    ValidateFile[addr.file];
    ValidatePageNumber[addr.page];
    AcquireCache[];
    END;

  END.