/* REXX */
/*--------------------------------------------------------------------*/
/* ENQDIAG - z/OS ENQ / GRS dependency diagnostic                    */
/*                                                                    */
/* Author  : SARATH S                                                 */
/* Purpose : Trace ENQ dependency chains, identify terminal holders,  */
/*           measure wait time, and enrich likely blockers with       */
/*           active-address-space, WTOR and JES evidence.             */
/*                                                                    */
/* Examples:                                                          */
/*   TSO ENQDIAG                                                      */
/*   TSO ENQDIAG JOB CICDLDF                                         */
/*   TSO ENQDIAG JOB CICDLDF SYS MVSD                                */
/*   TSO ENQDIAG DSN CICDL.DLDF.STARTUP                              */
/*   TSO ENQDIAG SAMPLE                                              */
/*   TSO ENQDIAG HELP                                                */
/*                                                                    */
/* Output:                                                            */
/*   <userid>.ENQDIAG.RPT    - persistent latest diagnostic report.   */
/*   <userid>.ENQDIAG.SAMPLE - persistent sample report.              */
/*   <userid>.ENQDIAG.HELP   - persistent README / HELP text.         */
/*                                                                    */
/* Safety:                                                            */
/*   Read-only. ENQDIAG never issues CANCEL, FORCE, DEQ, SETGRS,      */
/*   REPLY or any command that changes workload/system state.         */
/*--------------------------------------------------------------------*/

numeric digits 20
signal on syntax name FatalSyntax

/*--------------------------- Defaults ------------------------------*/
mode = 'ALL'
targetJob = ''
targetSys = ''
targetJobid = ''
targetAsid = ''
targetDsn = ''
analysisCount = 10
sampleCount = 2
sampleInterval = 5
maxEnrich = 3
maxSpoolLines = 1000
maxSpoolHits = 6

rpt. = ''
rpt.0 = 0
rptCount = 0
ispfActive = 0
sdsfOn = 0
ussOn = 0
localSys = translate(MVSVAR('SYSNAME'))
uid = sysvar('SYSUID')
reportDsn = uid'.ENQDIAG.RPT'
sampleDsn = uid'.ENQDIAG.SAMPLE'
helpDsn = uid'.ENQDIAG.HELP'

if sysvar('SYSISPF') = 'ACTIVE' then ispfActive = 1

parse arg rawArgs

call InitGraph
call ParseArguments rawArgs
if result <> 0 then do
  call PresentReport
  exit 12
end

if mode = 'HELP' then do
  call HelpReport
  call PresentReport
  exit 0
end

if mode = 'SAMPLE' then do
  call SampleReport
  call PresentReport
  exit 0
end

/*------------------------- SDSF startup -----------------------------*/
rc = isfcalls('ON')
if rc <> 0 then do
  call Emit 'ENQDIAG: Unable to establish SDSF REXX environment.'
  call Emit 'RC='rc
  call PresentReport
  exit 20
end
sdsfOn = 1

isfprefix = '**'
isfowner = '*'
isfdelay = '5'

/* USS sleep is optional. One DA sample is still useful without it. */
ussRc = syscalls('ON')
if ussRc = 0 then ussOn = 1
else do
  ussOn = 0
  sampleCount = 1
end

/*----------------------- Determine GRS command ----------------------*/
if mode = 'JOB' then do
  call ResolveJobSystem
  rjRc = result

  if rjRc <> 0 then do
    call Header
    call Section 'TARGET RESOLUTION'
    call Emit 'ENQDIAG could not uniquely determine the system on which'
    call Emit 'the requested job should be analyzed.'
    call Emit ''
    if targetSys = '' then do
      call Emit 'Retry with the executing system, for example:'
      call Emit '  TSO ENQDIAG JOB 'targetJob' SYS MVSD'
    end
    call Emit ''
    call Emit 'No recovery command was issued.'
    call Footer
    call Cleanup
    call PresentReport
    exit 8
  end

  grsCommand = 'D GRS,ANALYZE,DEPENDENCY,SYS='targetSys','
  if targetAsid <> '' then
    grsCommand = grsCommand'ASID='targetAsid',COUNT='analysisCount',DETAIL'
  else
    grsCommand = grsCommand'JOB='targetJob',COUNT='analysisCount',DETAIL'
end
else if mode = 'DSN' then do
  grsCommand = 'D GRS,ANALYZE,DEPENDENCY,RES=(SYSDSN,'
  grsCommand = grsCommand||targetDsn||'),COUNT='analysisCount',DETAIL'
end
else do
  grsCommand = 'D GRS,ANALYZE,DEPENDENCY,COUNT='analysisCount',DETAIL'
end

call InitGraph
call CaptureGRS grsCommand
if result <> 0 then do
  call Header
  call Section 'GRS ANALYSIS'
  call Emit 'Unable to capture D GRS,ANALYZE,DEPENDENCY output.'
  call KV 'Command', grsCommand
  call KV 'Command RC', grsRc
  call Emit ''
  call Emit 'Possible causes include OPERCMDS/SDSF authority, console'
  call Emit 'activation failure, or an unsupported command level.'
  call Footer
  call Cleanup
  call PresentReport
  exit 20
end

call ParseGRS
call ComputeGraph
call SelectEnrichmentNodes

/*-------------------- Enrich terminal holders ----------------------*/
if enrichCount > 0 then do
  do s = 1 to sampleCount
    call FetchDAForEnrichment s
    if s < sampleCount then call SleepSeconds sampleInterval
  end

  call EvaluateActivity
  call FetchSREvidence

  do ei = 1 to enrichCount
    n = enrichNode.ei
    call TrySpoolEvidence n

    if nodeDAFound.n = 0 then call RemoteDAEvidence n

    if mode <> 'ALL' & nodeWtorCount.n = 0 then do
      if nodeSys.n <> '' & nodeSys.n <> localSys then
        call RemoteReplyEvidence n
    end
  end
end

/*------------------------------ Report ------------------------------*/
call Header
call TriageSummary
call PriorityFindings
call DependencyDetails
call TerminalBlockers
call EnrichmentReport
call RawGRSHighlights
call Interpretation
call Recommendations
call PossibleResolution
call CommandReference
call Notes
call Footer
call Cleanup
call PresentReport
exit 0


/*====================================================================*/
/* ARGUMENTS                                                          */
/*====================================================================*/

ParseArguments:
parse upper arg paRaw
paRaw = strip(paRaw)

if paRaw = '' then return 0

paFirst = word(paRaw,1)

if paFirst = 'HELP' | paFirst = 'H' | paFirst = '?' then do
  mode = 'HELP'
  return 0
end

if paFirst = 'SAMPLE' then do
  mode = 'SAMPLE'
  return 0
end

if paFirst = 'ALL' then do
  mode = 'ALL'
  paStart = 2
end
else if paFirst = 'JOB' then do
  mode = 'JOB'
  targetJob = word(paRaw,2)
  if targetJob = '' then do
    call Emit 'ENQDIAG: JOB requires a jobname.'
    return 12
  end
  paStart = 3
end
else if paFirst = 'DSN' then do
  mode = 'DSN'
  targetDsn = word(paRaw,2)
  if targetDsn = '' then do
    call Emit 'ENQDIAG: DSN requires a data set name.'
    return 12
  end
  paStart = 3
end
else do
  /* Convenience: ENQDIAG CICDLDF is treated as JOB CICDLDF. */
  mode = 'JOB'
  targetJob = paFirst
  paStart = 2
end

paWords = words(paRaw)
paI = paStart

do while paI <= paWords
  paKey = word(paRaw,paI)
  paVal = word(paRaw,paI+1)

  select
    when paKey = 'SYS' | paKey = 'SYSTEM' then do
      targetSys = paVal
      paI = paI + 2
    end

    when paKey = 'JOBID' then do
      targetJobid = paVal
      paI = paI + 2
    end

    when paKey = 'ASID' then do
      targetAsid = StripHex(paVal)
      paI = paI + 2
    end

    when paKey = 'COUNT' then do
      if datatype(paVal,'W') = 0 then do
        call Emit 'ENQDIAG: COUNT must be a whole number.'
        return 12
      end
      analysisCount = paVal
      if analysisCount < 1 then analysisCount = 1
      if analysisCount > 99 then analysisCount = 99
      paI = paI + 2
    end

    when paKey = 'SAMPLES' then do
      if datatype(paVal,'W') = 0 then do
        call Emit 'ENQDIAG: SAMPLES must be a whole number.'
        return 12
      end
      sampleCount = paVal
      if sampleCount < 1 then sampleCount = 1
      if sampleCount > 5 then sampleCount = 5
      paI = paI + 2
    end

    when paKey = 'INTERVAL' then do
      if datatype(paVal,'W') = 0 then do
        call Emit 'ENQDIAG: INTERVAL must be a whole number.'
        return 12
      end
      sampleInterval = paVal
      if sampleInterval < 2 then sampleInterval = 2
      if sampleInterval > 30 then sampleInterval = 30
      paI = paI + 2
    end

    otherwise do
      call Emit 'ENQDIAG: Unknown parameter: 'paKey
      return 12
    end
  end
end

if length(targetJob) > 8 then do
  call Emit 'ENQDIAG: Jobname cannot exceed 8 characters.'
  return 12
end

if length(targetSys) > 8 then do
  call Emit 'ENQDIAG: System name cannot exceed 8 characters.'
  return 12
end

return 0


/*====================================================================*/
/* JOB SYSTEM RESOLUTION                                              */
/*====================================================================*/

ResolveJobSystem:
if targetSys <> '' then return 0

call FetchDARows
daRc = result

sysCand. = ''
sysCandCount = 0

if daRc = 0 then do
  do rj = 1 to isfrows
    rjJob = GetVar('JNAME',rj,'')
    if translate(rjJob) <> targetJob then iterate

    if targetJobid <> '' then do
      rjId = GetVar('JOBID',rj,'')
      if translate(rjId) <> targetJobid then iterate
    end

    if targetAsid <> '' then do
      rjAx = StripHex(GetVar('ASIDX',rj,''))
      rjAd = StripHex(GetVar('ASID',rj,''))
      if rjAx <> targetAsid & rjAd <> targetAsid then iterate
    end

    rjSys = translate(GetVar('SYSNAME',rj,''))
    if rjSys = '' then iterate

    call AddUnique 'sysCand.', sysCandCount, rjSys
    sysCandCount = result
  end
end

if sysCandCount = 1 then do
  targetSys = sysCand.1
  return 0
end

/* DA may not expose the remote system when RMF/SDSFAUX scope is */
/* limited. Use a broad GRS dependency display as a discovery fallback. */
call InitGraph
probeCmd = 'D GRS,ANALYZE,DEPENDENCY,COUNT='analysisCount',DETAIL'
call CaptureGRS probeCmd
if result <> 0 then return 8
call ParseGRS

sysCand. = ''
sysCandCount = 0

do rr = 1 to rootCount
  rn = rootNode.rr
  if translate(nodeJob.rn) <> targetJob then iterate

  if targetAsid <> '' then do
    if StripHex(nodeAsid.rn) <> targetAsid then iterate
  end

  if nodeSys.rn = '' then iterate
  call AddUnique 'sysCand.', sysCandCount, nodeSys.rn
  sysCandCount = result
end

if sysCandCount = 1 then do
  targetSys = sysCand.1
  return 0
end

return 8


/*====================================================================*/
/* GRS COMMAND CAPTURE                                                */
/*====================================================================*/

CaptureGRS:
parse arg cgCmd

drop ISFULOG.
grsRc = -1
grsRaw. = ''
grsRaw.0 = 0
grsRawCount = 0

isfdelay = '5'
Address SDSF "ISFSLASH '"cgCmd"'"
grsRc = rc

if grsRc <> 0 then return grsRc
if symbol('ISFULOG.0') <> 'VAR' then return 8
if isfulog.0 <= 0 then return 8

cgStart = 1

do cgI = 1 to isfulog.0
  cgU = translate(isfulog.cgI)
  if pos('ISG349I',cgU) > 0 & pos('GRS ANALYSIS',cgU) > 0 then
    cgStart = cgI
end

do cgI = cgStart to isfulog.0
  grsRawCount = grsRawCount + 1
  grsRaw.grsRawCount = strip(isfulog.cgI)
end

grsRaw.0 = grsRawCount
if grsRawCount = 0 then return 8
return 0


/*====================================================================*/
/* GRS DEPENDENCY PARSER                                              */
/*====================================================================*/

ParseGRS:
pgInWaiter = 0
pgNeedSource = 0
pgExpectRes = 0
pgExpectHolder = 0
pgCurrentNode = 0
pgSourceNode = 0
pgTmpSys = ''
pgWait = ''
pgQ = ''
pgR = ''
pgReq = ''
pgScope = ''
grsScopeText = ''
grsDeadlock = 0

 do pgI = 1 to grsRawCount
  pgLine = strip(grsRaw.pgI)
  pgU = translate(pgLine)

  if pos('DEPENDENCY ANALYSIS:',pgU) > 0 then do
    pgP = pos('DEPENDENCY ANALYSIS:',pgU)
    grsScopeText = strip(substr(pgLine,pgP+20))
    iterate
  end

  if pos('DEADLOCK',pgU) > 0 & pos('NO DEADLOCK',pgU) = 0 then
    grsDeadlock = 1

  if pos('LONG WAITER #',pgU) > 0 then do
    pgInWaiter = 1
    pgNeedSource = 1
    pgExpectRes = 0
    pgExpectHolder = 0
    pgCurrentNode = 0
    pgSourceNode = 0
    pgTmpSys = ''
    pgWait = ''
    pgQ = ''
    pgR = ''
    pgReq = ''
    pgScope = ''
    iterate
  end

  if pgInWaiter = 0 then iterate

  if pos('SYSTEM:',pgU) > 0 then do
    pgP = pos('SYSTEM:',pgU)
    pgTmpSys = strip(word(substr(pgLine,pgP+7),1))
    pgTmpSys = translate(pgTmpSys)
    iterate
  end

  if pos('JOBNAME:',pgU) > 0 then do
    call ParseJobLine pgLine
    pgJob = result
    pgAs = parsedAsid
    pgTc = parsedTcb

    if pgExpectHolder = 1 then do
      call AddNode pgTmpSys, pgJob, pgAs, pgTc
      pgHolder = result
      call AddEdge pgCurrentNode, pgHolder, pgQ, pgR, pgWait,
                   pgScope, pgReq
      pgCurrentNode = pgHolder
      pgExpectHolder = 0
      pgTmpSys = ''
    end
    else if pgNeedSource = 1 then do
      call AddNode pgTmpSys, pgJob, pgAs, pgTc
      pgSourceNode = result
      pgCurrentNode = pgSourceNode
      call AddRoot pgSourceNode, ''
      pgNeedSource = 0
      pgTmpSys = ''
    end
    iterate
  end

  if pos('REQUEST:',pgU) > 0 then do
    pgP = pos('REQUEST:',pgU)
    pgReq = strip(word(substr(pgLine,pgP+8),1))
    iterate
  end

  if pos('WAITING ',pgU) > 0 & pos(' FOR ',pgU) > 0 then do
    pgP = pos('WAITING ',pgU)
    pgRest = strip(substr(pgLine,pgP+8))
    pgWait = word(pgRest,1)
    pgScope = ''

    pgFor = pos(' FOR ',translate(pgRest))
    pgRes = pos(' RESOURCE',translate(pgRest))
    if pgFor > 0 & pgRes > pgFor then
      pgScope = strip(substr(pgRest,pgFor+5,pgRes-pgFor-5))

    if pgSourceNode > 0 then call AddRoot pgSourceNode, pgWait
    pgExpectRes = 1
    iterate
  end

  if pgExpectRes = 1 then do
    if pgLine = '' then iterate
    if pos('ANALYSIS ENDED:',pgU) > 0 then do
      pgExpectRes = 0
      iterate
    end

    pgQ = word(pgLine,1)
    pgR = strip(substr(pgLine,length(pgQ)+1))

    if pgQ <> '' & pgR <> '' then do
      pgExpectRes = 0
      pgExpectHolder = 1
      pgTmpSys = ''
    end
    iterate
  end

  if pos('ANALYSIS ENDED:',pgU) > 0 then do
    if pgCurrentNode > 0 then nodeTerminal.pgCurrentNode = 1
    pgInWaiter = 0
    pgExpectHolder = 0
    pgExpectRes = 0
    iterate
  end
 end
return


ParseJobLine:
parse arg pjlLine
parsedAsid = ''
parsedTcb = ''
pjlU = translate(pjlLine)
pjlP = pos('JOBNAME:',pjlU)
if pjlP = 0 then return ''

pjlRest = strip(substr(pjlLine,pjlP+8))
pjlJob = word(pjlRest,1)

pjlA = pos('ASID=',translate(pjlRest))
if pjlA > 0 then do
  pjlX = substr(pjlRest,pjlA+5)
  pjlC = pos(',',pjlX)
  pjlR = pos(')',pjlX)
  if pjlC > 0 then parsedAsid = strip(substr(pjlX,1,pjlC-1))
  else if pjlR > 0 then parsedAsid = strip(substr(pjlX,1,pjlR-1))
  else parsedAsid = word(pjlX,1)
  parsedAsid = StripHex(parsedAsid)
end

pjlT = pos('TCB=',translate(pjlRest))
if pjlT > 0 then do
  pjlX = substr(pjlRest,pjlT+4)
  pjlR = pos(')',pjlX)
  pjlC = pos(',',pjlX)
  if pjlR > 0 then parsedTcb = strip(substr(pjlX,1,pjlR-1))
  else if pjlC > 0 then parsedTcb = strip(substr(pjlX,1,pjlC-1))
  else parsedTcb = word(pjlX,1)
  parsedTcb = StripHex(parsedTcb)
end

return translate(pjlJob)


/*====================================================================*/
/* GRAPH STORAGE                                                      */
/*====================================================================*/

InitGraph:
nodeCount = 0
nodeSys. = ''
nodeJob. = ''
nodeAsid. = ''
nodeTcb. = ''
nodeTerminal. = 0
nodeDAFound. = 0
nodeDAJobid. = ''
nodeElapsed. = ''
nodeStart. = ''
nodeStatus. = ''
nodeSrv. = ''
nodeCpuLimit. = ''
nodeCpu1. = ''
nodeCpu2. = ''
nodeCpu3. = ''
nodeExcp1. = ''
nodeExcp2. = ''
nodeExcp3. = ''
nodeCpuPr. = ''
nodeSio. = ''
nodeCpuDelta. = ''
nodeExcpDelta. = ''
nodeActivity. = 'NOT SAMPLED'
nodeWtorCount. = 0
nodeWtorExact. = 0
nodeWtor1. = ''
nodeWtor2. = ''
nodeWtor3. = ''
nodeSpool. = 'NOT CHECKED'
nodeSpoolHits. = 0
nodeSpoolHit. = ''
nodeRemoteDA. = 'NOT NEEDED'
nodeRemoteReply. = 'NOT CHECKED'
nodeImpact. = 0
nodeDirect. = 0
nodeMaxWait. = 0
nodeAssessment. = ''
nodeConfidence. = ''

edgeCount = 0
edgeFrom. = 0
edgeTo. = 0
edgeQ. = ''
edgeR. = ''
edgeWait. = ''
edgeWaitSec. = -1
edgeScope. = ''
edgeReq. = ''

rootCount = 0
rootNode. = 0
rootWait. = ''
rootWaitSec. = -1
rootResourceCount. = 0

enrichCount = 0
enrichNode. = 0
terminalCount = 0
terminalNode. = 0
terminalRank. = 0

grsRaw. = ''
grsRaw.0 = 0
grsRawCount = 0
grsScopeText = ''
grsDeadlock = 0
grsRc = -1
return


AddNode:
parse arg anSys, anJob, anAsid, anTcb
anSys = translate(strip(anSys))
anJob = translate(strip(anJob))
anAsid = StripHex(anAsid)
anTcb = StripHex(anTcb)

if anSys = '' then anSys = '?'
if anJob = '' then anJob = '?'

 do anI = 1 to nodeCount
  if nodeSys.anI = anSys & nodeJob.anI = anJob & ,
     nodeAsid.anI = anAsid & nodeTcb.anI = anTcb then return anI
 end

nodeCount = nodeCount + 1
nodeSys.nodeCount = anSys
nodeJob.nodeCount = anJob
nodeAsid.nodeCount = anAsid
nodeTcb.nodeCount = anTcb
return nodeCount


AddEdge:
parse arg aeFrom, aeTo, aeQ, aeR, aeWait, aeScope, aeReq

if aeFrom <= 0 | aeTo <= 0 then return 0

aeQ = translate(strip(aeQ))
aeR = strip(aeR)
aeWait = strip(aeWait)

 do aeI = 1 to edgeCount
  if edgeFrom.aeI = aeFrom & edgeTo.aeI = aeTo & ,
     edgeQ.aeI = aeQ & edgeR.aeI = aeR then return aeI
 end

edgeCount = edgeCount + 1
edgeFrom.edgeCount = aeFrom
edgeTo.edgeCount = aeTo
edgeQ.edgeCount = aeQ
edgeR.edgeCount = aeR
edgeWait.edgeCount = aeWait
edgeWaitSec.edgeCount = WaitToSeconds(aeWait)
edgeScope.edgeCount = strip(aeScope)
edgeReq.edgeCount = strip(aeReq)
return edgeCount


AddRoot:
parse arg arNode, arWait
if arNode <= 0 then return 0
arSec = WaitToSeconds(arWait)

 do arI = 1 to rootCount
  if rootNode.arI = arNode then do
    if arSec > rootWaitSec.arI then do
      rootWait.arI = arWait
      rootWaitSec.arI = arSec
    end
    return arI
  end
 end

rootCount = rootCount + 1
rootNode.rootCount = arNode
rootWait.rootCount = arWait
rootWaitSec.rootCount = arSec
return rootCount


/*====================================================================*/
/* GRAPH ANALYSIS                                                     */
/*====================================================================*/

ComputeGraph:
/* Count root resources. */
 do cr = 1 to rootCount
  rn = rootNode.cr
  rcnt = 0
  do ce = 1 to edgeCount
    if edgeFrom.ce = rn then rcnt = rcnt + 1
  end
  rootResourceCount.cr = rcnt
 end

/* Build terminal list. */
terminalCount = 0
 do cn = 1 to nodeCount
  if nodeTerminal.cn then do
    terminalCount = terminalCount + 1
    terminalNode.terminalCount = cn
  end
 end

/* For each terminal, walk backwards and count unique impacted nodes. */
 do ct = 1 to terminalCount
  tn = terminalNode.ct
  seen. = 0
  queue. = 0
  qh = 1
  qt = 1
  queue.1 = tn
  seen.tn = 1
  impact = 0
  direct. = 0
  directCount = 0
  maxWait = -1

  do while qh <= qt
    cur = queue.qh
    qh = qh + 1

    do ce = 1 to edgeCount
      if edgeTo.ce <> cur then iterate

      src = edgeFrom.ce
      if edgeWaitSec.ce > maxWait then maxWait = edgeWaitSec.ce

      if cur = tn & direct.src = 0 then do
        direct.src = 1
        directCount = directCount + 1
      end

      if seen.src = 0 then do
        seen.src = 1
        impact = impact + 1
        qt = qt + 1
        queue.qt = src
      end
    end
  end

  nodeImpact.tn = impact
  nodeDirect.tn = directCount
  nodeMaxWait.tn = maxWait
 end

/* Rank terminals by impacted nodes, then oldest upstream wait. */
usedTerm. = 0
 do rank = 1 to terminalCount
  bestT = 0
  bestScore = -1

  do ct = 1 to terminalCount
    if usedTerm.ct then iterate
    tn = terminalNode.ct
    score = nodeImpact.tn * 1000000000
    if nodeMaxWait.tn > 0 then score = score + nodeMaxWait.tn
    if score > bestScore then do
      bestScore = score
      bestT = ct
    end
  end

  if bestT = 0 then leave
  usedTerm.bestT = 1
  terminalRank.rank = terminalNode.bestT
 end
return


SelectEnrichmentNodes:
enrichCount = 0
if terminalCount = 0 then return

selMax = maxEnrich
if mode <> 'ALL' then selMax = 8
if selMax > terminalCount then selMax = terminalCount

 do se = 1 to selMax
  if terminalRank.se <= 0 then iterate
  enrichCount = enrichCount + 1
  enrichNode.enrichCount = terminalRank.se
 end
return


FindRootTerminal:
parse arg frtRoot
seen. = 0
queue. = 0
qh = 1
qt = 1
queue.1 = frtRoot
seen.frtRoot = 1
best = 0
bestImpact = -1
termFound = 0

 do while qh <= qt
  cur = queue.qh
  qh = qh + 1

  if nodeTerminal.cur then do
    termFound = termFound + 1
    if nodeImpact.cur > bestImpact then do
      bestImpact = nodeImpact.cur
      best = cur
    end
  end

  do fe = 1 to edgeCount
    if edgeFrom.fe <> cur then iterate
    nxt = edgeTo.fe
    if seen.nxt = 0 then do
      seen.nxt = 1
      qt = qt + 1
      queue.qt = nxt
    end
  end
 end

rootTerminalCount = termFound
return best


/*====================================================================*/
/* SDSF DA                                                            */
/*====================================================================*/

FetchDARows:
drop JNAME. JOBID. ASID. ASIDX. SYSNAME. CPU. EXCP. CPUPR. EXCPRT.
drop STDATE. ELAPSED. CPULIMIT. SRVCLASS. STATUS.

isfcols = 'JNAME JOBID ASID ASIDX SYSNAME CPU EXCP CPUPR EXCPRT '
isfcols = isfcols'STDATE ELAPSED CPULIMIT SRVCLASS STATUS'
Address SDSF 'ISFEXEC DA'
fdrRc = rc
if fdrRc = 0 then return 0

drop JNAME. JOBID. ASID. ASIDX. SYSNAME. CPU. EXCP. CPUPR. EXCPRT.
drop STDATE. ELAPSED. STATUS.
isfcols = 'JNAME JOBID ASID ASIDX SYSNAME CPU EXCP CPUPR EXCPRT '
isfcols = isfcols'STDATE ELAPSED STATUS'
Address SDSF 'ISFEXEC DA'
fdrRc = rc
if fdrRc = 0 then return 0

drop JNAME. JOBID. ASID. ASIDX. CPU. EXCP. STATUS.
isfcols = 'JNAME JOBID ASID ASIDX CPU EXCP STATUS'
Address SDSF 'ISFEXEC DA'
return rc


FetchDAForEnrichment:
parse arg fdSample
call FetchDARows
fdRc = result
if fdRc <> 0 then return fdRc

 do fdE = 1 to enrichCount
  fdN = enrichNode.fdE
  fdBest = 0

  do fdR = 1 to isfrows
    fdJob = translate(GetVar('JNAME',fdR,''))
    if fdJob <> nodeJob.fdN then iterate

    fdSys = translate(GetVar('SYSNAME',fdR,''))
    fdAx = StripHex(GetVar('ASIDX',fdR,''))
    fdAd = StripHex(GetVar('ASID',fdR,''))

    if nodeSys.fdN <> '?' & fdSys <> '' then do
      if fdSys <> nodeSys.fdN then iterate
    end

    if nodeAsid.fdN <> '' then do
      if fdAx <> '' | fdAd <> '' then do
        if fdAx <> nodeAsid.fdN & fdAd <> nodeAsid.fdN then iterate
      end
    end

    fdBest = fdR
    leave
  end

  if fdBest = 0 then iterate

  nodeDAFound.fdN = 1
  nodeDAJobid.fdN = GetVar('JOBID',fdBest,nodeDAJobid.fdN)
  nodeElapsed.fdN = GetVar('ELAPSED',fdBest,nodeElapsed.fdN)
  nodeStart.fdN = GetVar('STDATE',fdBest,nodeStart.fdN)
  nodeStatus.fdN = GetVar('STATUS',fdBest,nodeStatus.fdN)
  nodeSrv.fdN = GetVar('SRVCLASS',fdBest,nodeSrv.fdN)
  nodeCpuLimit.fdN = GetVar('CPULIMIT',fdBest,nodeCpuLimit.fdN)
  nodeCpuPr.fdN = GetVar('CPUPR',fdBest,nodeCpuPr.fdN)
  nodeSio.fdN = GetVar('EXCPRT',fdBest,nodeSio.fdN)

  fdCpu = GetVar('CPU',fdBest,'')
  fdExcp = GetVar('EXCP',fdBest,'')

  if fdSample = 1 then do
    nodeCpu1.fdN = fdCpu
    nodeExcp1.fdN = fdExcp
  end
  else if fdSample = 2 then do
    nodeCpu2.fdN = fdCpu
    nodeExcp2.fdN = fdExcp
  end
  else do
    nodeCpu3.fdN = fdCpu
    nodeExcp3.fdN = fdExcp
  end
 end
return 0


EvaluateActivity:
 do ea = 1 to enrichCount
  n = enrichNode.ea

  if nodeDAFound.n = 0 then do
    nodeActivity.n = 'UNKNOWN - DA DATA NOT AVAILABLE'
    iterate
  end

  if sampleCount = 1 then do
    nodeActivity.n = 'SINGLE SAMPLE ONLY'
    iterate
  end

  if sampleCount = 2 then do
    c1 = ScaledNumber(nodeCpu1.n)
    c2 = ScaledNumber(nodeCpu2.n)
    x1 = ScaledNumber(nodeExcp1.n)
    x2 = ScaledNumber(nodeExcp2.n)
  end
  else do
    c1 = ScaledNumber(nodeCpu1.n)
    c2 = ScaledNumber(nodeCpu3.n)
    x1 = ScaledNumber(nodeExcp1.n)
    x2 = ScaledNumber(nodeExcp3.n)
  end

  if c1 < 0 | c2 < 0 then cDelta = -1
  else cDelta = c2 - c1

  if x1 < 0 | x2 < 0 then xDelta = -1
  else xDelta = x2 - x1

  if cDelta >= 0 then nodeCpuDelta.n = cDelta
  if xDelta >= 0 then nodeExcpDelta.n = xDelta

  if cDelta > 0.01 | xDelta > 0 then
    nodeActivity.n = 'ACTIVE / PROGRESS OBSERVED'
  else if cDelta >= 0 & xDelta >= 0 then
    nodeActivity.n = 'NO CPU OR EXCP PROGRESS OBSERVED'
  else
    nodeActivity.n = 'PARTIAL DATA - PROGRESS UNDETERMINED'
 end
return


/*====================================================================*/
/* SDSF SR / OUTSTANDING REQUESTS                                     */
/*====================================================================*/

FetchSREvidence:
drop REPLYID. SYSNAME. JNAME. MSGTEXT. JOBID. DATEE. TIMEE.
drop MSGTYPE. ELAPSED.

isfcols = 'REPLYID SYSNAME JNAME MSGTEXT JOBID DATEE TIMEE '
isfcols = isfcols'MSGTYPE ELAPSED'
Address SDSF 'ISFEXEC SR'
fsRc = rc

if fsRc <> 0 then do
  drop REPLYID. SYSNAME. JNAME. MSGTEXT. JOBID. DATEE. TIMEE.
  drop MSGTYPE.
  isfcols = 'REPLYID SYSNAME JNAME MSGTEXT JOBID DATEE TIMEE MSGTYPE'
  Address SDSF 'ISFEXEC SR'
  fsRc = rc
end

if fsRc <> 0 then return fsRc

 do fsE = 1 to enrichCount
  n = enrichNode.fsE

  do fsR = 1 to isfrows
    fsJob = translate(GetVar('JNAME',fsR,''))
    fsSys = translate(GetVar('SYSNAME',fsR,''))
    fsId = translate(GetVar('JOBID',fsR,''))

    if fsJob <> nodeJob.n then iterate
    if nodeSys.n <> '?' & fsSys <> '' then do
      if fsSys <> nodeSys.n then iterate
    end

    if nodeDAJobid.n <> '' & fsId <> '' then do
      if fsId <> translate(nodeDAJobid.n) then iterate
      nodeWtorExact.n = 1
    end

    nodeWtorCount.n = nodeWtorCount.n + 1
    wc = nodeWtorCount.n

    fsText = GetVar('MSGTEXT',fsR,'')
    fsReply = GetVar('REPLYID',fsR,'')
    fsElapsed = GetVar('ELAPSED',fsR,'')

    fsLine = 'Reply='fsReply
    if fsElapsed <> '' then fsLine = fsLine' Age='fsElapsed
    fsLine = fsLine' 'fsText

    if wc = 1 then nodeWtor1.n = fsLine
    if wc = 2 then nodeWtor2.n = fsLine
    if wc = 3 then nodeWtor3.n = fsLine
  end
 end
return 0


/*====================================================================*/
/* JES SPOOL EVIDENCE                                                 */
/*====================================================================*/

TrySpoolEvidence:
parse arg tsNode
nodeSpool.tsNode = 'NOT ACCESSIBLE FROM CURRENT SDSF/JES'
nodeSpoolHits.tsNode = 0

oldPrefix = isfprefix
isfprefix = nodeJob.tsNode

drop JNAME. JOBID. QUEUE. ACTSYS. SYSNAME. ASIDX. TOKEN.
isfcols = 'JNAME JOBID QUEUE ACTSYS SYSNAME ASIDX'
Address SDSF 'ISFEXEC ST'
tsRc = rc

if tsRc <> 0 then do
  drop JNAME. JOBID. QUEUE. ACTSYS. ASIDX. TOKEN.
  isfcols = 'JNAME JOBID QUEUE ACTSYS ASIDX'
  Address SDSF 'ISFEXEC ST'
  tsRc = rc
end

isfprefix = oldPrefix
if tsRc <> 0 then return tsRc

tsRow = 0
 do tsR = 1 to isfrows
  tsJob = translate(GetVar('JNAME',tsR,''))
  if tsJob <> nodeJob.tsNode then iterate

  tsSys = translate(GetVar('SYSNAME',tsR,''))
  tsAx = StripHex(GetVar('ASIDX',tsR,''))

  if nodeSys.tsNode <> '?' & tsSys <> '' then do
    if tsSys <> nodeSys.tsNode then iterate
  end

  if nodeAsid.tsNode <> '' & tsAx <> '' then do
    if tsAx <> nodeAsid.tsNode then iterate
  end

  if nodeDAJobid.tsNode <> '' then do
    tsId = translate(GetVar('JOBID',tsR,''))
    if tsId <> '' & tsId <> translate(nodeDAJobid.tsNode) then iterate
  end

  tsRow = tsR
  leave
 end

if tsRow = 0 then return 4
if symbol('TOKEN.'tsRow) <> 'VAR' then return 8

nodeSpool.tsNode = 'ACCESSIBLE'

drop JDS_DDNAME. JDS_TOKEN. JDS_DSNAME.
Address SDSF "ISFACT ST TOKEN('"TOKEN.tsRow"') PARM(NP ?) " ,
             '( PREFIX JDS_'
tsRc = rc
if tsRc <> 0 then do
  nodeSpool.tsNode = 'ST ROW FOUND; JDS ACCESS FAILED'
  return tsRc
end

if symbol('JDS_DDNAME.0') <> 'VAR' then do
  nodeSpool.tsNode = 'JDS RETURNED NO DATA SET LIST'
  return 8
end

isflinelim = 500

 do tsJ = 1 to jds_DDNAME.0
  tsDD = translate(jds_DDNAME.tsJ)
  if tsDD <> 'JESMSGLG' & tsDD <> 'JESYSMSG' then iterate

  isfstartlinetoken = ''
  tsRead = 0

  do forever
    Address SDSF "ISFBROWSE ST TOKEN('"jds_TOKEN.tsJ"')"
    tsBrc = rc
    if tsBrc <> 0 then leave
    if symbol('ISFLINE.0') <> 'VAR' then leave

    do tsK = 1 to isfline.0
      tsRead = tsRead + 1
      if tsRead > maxSpoolLines then leave
      call CheckSpoolLine tsNode, tsDD, isfline.tsK
    end

    if tsRead >= maxSpoolLines then leave
    if isfnextlinetoken = '' then leave
    isfstartlinetoken = isfnextlinetoken
  end
 end
return 0


CheckSpoolLine:
parse arg csNode, csDD, csLine
if nodeSpoolHits.csNode >= maxSpoolHits then return

csU = translate(csLine)
csHit = 0

if pos('IEF861I',csU) > 0 then csHit = 1
if pos('IEF863I',csU) > 0 then csHit = 1
if pos('IEF458D',csU) > 0 then csHit = 1
if pos('WAITING FOR DATA SET',csU) > 0 then csHit = 1
if pos('WAITING FOR DATASET',csU) > 0 then csHit = 1
if pos('EXCEEDED',csU) > 0 & pos('CPU',csU) > 0 then csHit = 1
if pos('REPLY',csU) > 0 & pos('ALLOW',csU) > 0 then csHit = 1
if pos('MOUNT',csU) > 0 & pos('REPLY',csU) > 0 then csHit = 1
if pos('ABEND',csU) > 0 then csHit = 1

if csHit = 0 then return

nodeSpoolHits.csNode = nodeSpoolHits.csNode + 1
csN = nodeSpoolHits.csNode
nodeSpoolHit.csNode.csN = csDD': 'ShortText(strip(csLine),112)
return


/*====================================================================*/
/* REMOTE READ-ONLY FALLBACKS                                         */
/*====================================================================*/

RemoteDAEvidence:
parse arg rdNode
if nodeSys.rdNode = '?' | nodeSys.rdNode = '' then return 4
if nodeSys.rdNode = localSys then return 4

rdCmd = 'RO 'nodeSys.rdNode',D A,'nodeJob.rdNode

drop ISFULOG.
isfdelay = '4'
Address SDSF "ISFSLASH '"rdCmd"'"
rdRc = rc

if rdRc <> 0 then do
  nodeRemoteDA.rdNode = 'FAILED / NOT AUTHORIZED'
  return rdRc
end

nodeRemoteDA.rdNode = 'COMMAND SUCCEEDED'
rdHits = 0
if symbol('ISFULOG.0') = 'VAR' then do
  do rdI = 1 to isfulog.0
    rdLine = strip(isfulog.rdI)
    rdU = translate(rdLine)
    if pos(nodeJob.rdNode,rdU) = 0 then iterate
    rdHits = rdHits + 1
    if rdHits <= 4 then
      nodeRemoteDALine.rdNode.rdHits = ShortText(rdLine,112)
  end
end
nodeRemoteDACount.rdNode = rdHits
return 0


RemoteReplyEvidence:
parse arg rrNode
if nodeSys.rrNode = '?' | nodeSys.rrNode = '' then return 4
if nodeSys.rrNode = localSys then return 4

rrCmd = 'RO 'nodeSys.rrNode',D R,R'

drop ISFULOG.
isfdelay = '4'
Address SDSF "ISFSLASH '"rrCmd"'"
rrRc = rc

if rrRc <> 0 then do
  nodeRemoteReply.rrNode = 'FAILED / NOT AUTHORIZED'
  return rrRc
end

rrHits = 0
if symbol('ISFULOG.0') = 'VAR' then do
  do rrI = 1 to isfulog.0
    rrLine = strip(isfulog.rrI)
    rrU = translate(rrLine)
    if pos(nodeJob.rrNode,rrU) = 0 then iterate
    rrHits = rrHits + 1
    if rrHits <= 3 then
      nodeRemoteReplyLine.rrNode.rrHits = ShortText(rrLine,112)
  end
end

nodeRemoteReplyCount.rrNode = rrHits
if rrHits > 0 then nodeRemoteReply.rrNode = 'MATCHING TEXT FOUND'
else nodeRemoteReply.rrNode = 'NO JOBNAME MATCH IN D R,R RESPONSE'
return 0


/*====================================================================*/
/* ASSESSMENT                                                         */
/*====================================================================*/

AssessNode:
parse arg asNode
asWait = nodeMaxWait.asNode
asElapsed = DurationToSeconds(nodeElapsed.asNode)

if nodeWtorCount.asNode > 0 then do
  if nodeWtorExact.asNode then do
    nodeAssessment.asNode = 'OPERATOR-ACTION EVIDENCE FOUND'
    nodeConfidence.asNode = 'HIGH'
  end
  else do
    nodeAssessment.asNode = 'POSSIBLE OPERATOR-ACTION EVIDENCE'
    nodeConfidence.asNode = 'MEDIUM'
  end
  return
end

if nodeActivity.asNode = 'ACTIVE / PROGRESS OBSERVED' then do
  nodeAssessment.asNode = 'ACTIVE TERMINAL HOLDER - VERIFY FIRST'
  nodeConfidence.asNode = 'MEDIUM'
  return
end

if nodeActivity.asNode = 'NO CPU OR EXCP PROGRESS OBSERVED' then do
  if asWait >= 3600 | asElapsed >= 14400 then do
    nodeAssessment.asNode = 'LIKELY STALLED TERMINAL HOLDER'
    nodeConfidence.asNode = 'MEDIUM'
    return
  end
end

nodeAssessment.asNode = 'TERMINAL HOLDER - CAUSE UNDETERMINED'
nodeConfidence.asNode = 'LOW-MEDIUM'
return


/*====================================================================*/
/* REPORT                                                             */
/*====================================================================*/

Header:
call Emit ''
call Emit copies('=',78)
call Emit ' ENQDIAG - ENQ / GRS DEPENDENCY DIAGNOSTIC'
call KV 'Author', 'SARATH S'
call KV 'Date', DisplayDate()
call KV 'Time', time('N')
call KV 'Executed from system', localSys
call KV 'Report data set', reportDsn
call KV 'Mode', mode
if mode = 'JOB' then do
  call KV 'Target job', targetJob
  call KV 'Target system', targetSys
end
if mode = 'DSN' then call KV 'Target data set', targetDsn
call Emit copies('=',78)
return


TriageSummary:
call Section 'EXECUTIVE SUMMARY'
call KV 'GRS analysis scope', ValueOr(grsScopeText,'UNKNOWN')
call KV 'Unique dependency roots', rootCount
call KV 'Unique units of work', nodeCount
call KV 'Unique dependency edges', edgeCount
call KV 'Terminal holders', terminalCount
call KV 'Deadlock text detected', YesNo(grsDeadlock)
call KV 'DA activity samples', sampleCount
obsWindow = (sampleCount-1) * sampleInterval
call KV 'Activity observation window', obsWindow' seconds'

if rootCount = 0 then do
  call Emit ''
  call Emit 'No GRS dependency chain was parsed from the command response.'
  call Emit 'If the reported work is still delayed, the cause may not be a'
  call Emit 'current GRS-managed ENQ dependency.'
end
return


PriorityFindings:
call Section 'PRIMARY FINDINGS'
if rootCount = 0 then do
  call Emit 'No long-waiter dependency roots were found.'
  return
end

call Emit 'Pri Waiter/ASID       System   Wait       Res  Terminal holder'
call Emit copies('-',78)

usedRoot. = 0
pfShow = rootCount
if pfShow > 15 then pfShow = 15

 do pfRank = 1 to pfShow
  pfBest = 0
  pfScore = -1

  do pf = 1 to rootCount
    if usedRoot.pf then iterate
    pfSec = rootWaitSec.pf
    if pfSec < 0 then pfSec = 0
    pfScore2 = pfSec * 1000 + rootResourceCount.pf
    if pfScore2 > pfScore then do
      pfBest = pf
      pfScore = pfScore2
    end
  end

  if pfBest = 0 then leave
  usedRoot.pfBest = 1
  rn = rootNode.pfBest
  call FindRootTerminal rn
  tn = result

  pfWaiter = nodeJob.rn'/'ValueOr(nodeAsid.rn,'----')
  if tn > 0 then do
    if rootTerminalCount = 1 then
      pfTerm = nodeJob.tn'/'ValueOr(nodeAsid.tn,'----')
    else
      pfTerm = 'MULTIPLE('rootTerminalCount')'
  end
  else pfTerm = 'NOT RESOLVED'

  pfLine = right(pfRank,4) left(pfWaiter,17) ,
           left(nodeSys.rn,8) left(ValueOr(rootWait.pfBest,'?'),10) ,
           right(rootResourceCount.pfBest,4) left(pfTerm,20)
  call Emit pfLine
 end
return


DependencyDetails:
call Section 'DETAILED ANALYSIS - DEPENDENCY CHAINS'
if rootCount = 0 then do
  call Emit 'No dependency chains to display.'
  return
end

showRoots = rootCount
if mode = 'ALL' & showRoots > 10 then showRoots = 10

 do dd = 1 to showRoots
  rn = rootNode.dd
  call Emit ''
  call Emit '['dd'] 'NodeLabel(rn)
  call KV 'Oldest parsed wait', ValueOr(rootWait.dd,'UNKNOWN')

  /* Breadth-first path display with depth. */
  seen. = 0
  qNode. = 0
  qDepth. = 0
  qh = 1
  qt = 1
  qNode.1 = rn
  qDepth.1 = 0
  seen.rn = 1
  printed = 0

  do while qh <= qt
    cur = qNode.qh
    dep = qDepth.qh
    qh = qh + 1

    do de = 1 to edgeCount
      if edgeFrom.de <> cur then iterate
      nxt = edgeTo.de
      printed = printed + 1
      if printed > 40 then leave

      dPrefix = copies(' ',dep*2)
      dLine = dPrefix' -> ['edgeQ.de'] '
      dLine = dLine||ShortText(edgeR.de,34)
      dLine = dLine'  wait='ValueOr(edgeWait.de,'?')
      call Emit dLine
      call Emit dPrefix'    HOLDER: 'NodeLabel(nxt)

      if seen.nxt = 0 then do
        seen.nxt = 1
        qt = qt + 1
        qNode.qt = nxt
        qDepth.qt = dep + 1
      end
    end

    if printed > 40 then leave
  end

  if printed > 40 then
    call Emit '  Additional dependency edges omitted from this section.'
 end
return


TerminalBlockers:
call Section 'DETAILED ANALYSIS - TERMINAL HOLDERS'
if terminalCount = 0 then do
  call Emit 'No terminal GRS holder was identified.'
  return
end

call Emit 'Rank Terminal holder        System  Impact Direct Oldest wait'
call Emit copies('-',78)

tbShow = terminalCount
if tbShow > 15 then tbShow = 15

 do tb = 1 to tbShow
  n = terminalRank.tb
  tbName = nodeJob.n'/'ValueOr(nodeAsid.n,'----')
  tbWait = SecondsToDuration(nodeMaxWait.n)
  tbLine = right(tb,4) left(tbName,22) left(nodeSys.n,8) ,
           right(nodeImpact.n,6) right(nodeDirect.n,6) ,
           left(tbWait,12)
  call Emit tbLine
 end

call Emit ''
call Emit 'Impact = unique upstream units of work that ultimately depend'
call Emit 'on this terminal holder within the parsed GRS graph.'
return


EnrichmentReport:
call Section 'SUPPORTING EVIDENCE'
if enrichCount = 0 then do
  call Emit 'No terminal holder was selected for enrichment.'
  return
end

 do er = 1 to enrichCount
  n = enrichNode.er
  call AssessNode n

  call Emit ''
  call Emit copies('-',78)
  call Emit ' 'NodeLabel(n)
  call Emit copies('-',78)
  call KV 'GRS role', 'TERMINAL HOLDER'
  call KV 'Upstream units affected', nodeImpact.n
  call KV 'Direct upstream units', nodeDirect.n
  call KV 'Oldest upstream wait', SecondsToDuration(nodeMaxWait.n)
  call KV 'DA data available', YesNo(nodeDAFound.n)

  if nodeDAFound.n then do
    call KV 'Job ID', ValueOr(nodeDAJobid.n,'-')
    call KV 'Address-space start', ValueOr(nodeStart.n,'-')
    call KV 'Address-space elapsed', ValueOr(nodeElapsed.n,'-')
    call KV 'Status', ValueOr(nodeStatus.n,'-')
    call KV 'Service class', ValueOr(nodeSrv.n,'-')
    call KV 'CPU limit', ValueOr(nodeCpuLimit.n,'-')
    call KV 'Latest CPU%', ValueOr(nodeCpuPr.n,'-')
    call KV 'Latest SIO', ValueOr(nodeSio.n,'-')

    call KV 'CPU sample 1', ValueOr(nodeCpu1.n,'-')
    if sampleCount >= 2 then
      call KV 'CPU final sample', FinalCpu(n)
    call KV 'EXCP sample 1', ValueOr(nodeExcp1.n,'-')
    if sampleCount >= 2 then
      call KV 'EXCP final sample', FinalExcp(n)
  end

  call KV 'Observed activity', nodeActivity.n
  if nodeCpuDelta.n <> '' then
    call KV 'Observed CPU delta', nodeCpuDelta.n' sec'
  if nodeExcpDelta.n <> '' then
    call KV 'Observed EXCP delta', nodeExcpDelta.n
  call KV 'Outstanding SR messages', nodeWtorCount.n
  if nodeWtorCount.n > 0 then
    call KV 'SR matched exact JobID', YesNo(nodeWtorExact.n)

  if nodeWtorCount.n > 0 then do
    if nodeWtor1.n <> '' then call Emit '  SR: 'ShortText(nodeWtor1.n,122)
    if nodeWtor2.n <> '' then call Emit '  SR: 'ShortText(nodeWtor2.n,122)
    if nodeWtor3.n <> '' then call Emit '  SR: 'ShortText(nodeWtor3.n,122)
    if nodeWtorCount.n > 3 then
      call Emit '  Additional matching SR messages omitted.'
  end

  call KV 'JES spool evidence', nodeSpool.n
  call KV 'Relevant spool lines', nodeSpoolHits.n

  do sh = 1 to nodeSpoolHits.n
    call Emit '  LOG: 'nodeSpoolHit.n.sh
  end

  if nodeDAFound.n = 0 then do
    call KV 'Remote D A fallback', nodeRemoteDA.n
    rdCnt = GetNested('nodeRemoteDACount',n,0)
    do rl = 1 to rdCnt
      if rl > 4 then leave
      call Emit '  RO:  'nodeRemoteDALine.n.rl
    end
  end

  if nodeRemoteReply.n <> 'NOT CHECKED' then do
    call KV 'Remote D R,R fallback', nodeRemoteReply.n
    rrCnt = GetNested('nodeRemoteReplyCount',n,0)
    do rl = 1 to rrCnt
      if rl > 3 then leave
      call Emit '  RO:  'nodeRemoteReplyLine.n.rl
    end
  end

  call KV 'Assessment', nodeAssessment.n
  call KV 'Assessment confidence', nodeConfidence.n

  if nodeSpool.n <> 'ACCESSIBLE' then do
    call Emit ''
    call Emit '  Spool note: JES output for this address space could not be'
    call Emit '  browsed from the current SDSF/JES view. GRS dependency'
    call Emit '  tracing remains valid; only joblog evidence is missing.'
  end
 end
return


RawGRSHighlights:
call Section 'COMMAND EVIDENCE'
call KV 'Command issued', grsCommand
call KV 'Response lines captured', grsRawCount

if grsRawCount = 0 then return

call Emit ''
call Emit 'Selected GRS lines:'
rhShown = 0
 do rh = 1 to grsRawCount
  rhU = translate(grsRaw.rh)
  rhKeep = 0
  if pos('DEPENDENCY ANALYSIS:',rhU) > 0 then rhKeep = 1
  if pos('LONG WAITER #',rhU) > 0 then rhKeep = 1
  if pos('WAITING ',rhU) > 0 then rhKeep = 1
  if pos('ANALYSIS ENDED:',rhU) > 0 then rhKeep = 1
  if pos('DEADLOCK',rhU) > 0 then rhKeep = 1

  if rhKeep then do
    rhShown = rhShown + 1
    if rhShown <= 40 then call Emit '  'ShortText(grsRaw.rh,124)
  end
 end

if rhShown > 40 then call Emit '  Additional GRS evidence lines omitted.'
return


Interpretation:
call Section 'INTERPRETATION'

if rootCount = 0 then do
  call Emit 'No current GRS dependency chain was identified.'
  call Emit 'If the work is still delayed, investigate non-GRS causes.'
  return
end

if terminalCount = 0 then do
  call Emit 'GRS dependency activity exists, but ENQDIAG did not identify'
  call Emit 'a terminal holder from the parsed response.'
  call Emit 'Review COMMAND EVIDENCE and the raw GRS dependency output.'
  return
end

topN = terminalRank.1
if topN > 0 then do
  call AssessNode topN
  call KV 'Highest-impact terminal holder', NodeLabel(topN)
  call KV 'Upstream units affected', nodeImpact.topN
  call KV 'Oldest upstream wait', SecondsToDuration(nodeMaxWait.topN)
  call KV 'Assessment', nodeAssessment.topN
  call KV 'Assessment confidence', nodeConfidence.topN
end

call Emit ''
call Emit 'Interpretation rule: a terminal holder is where this GRS'
call Emit 'dependency path ends. It is not automatically the faulty job.'
call Emit 'WTOR, joblog evidence, elapsed time and observed activity must'
call Emit 'be considered before any recovery decision.'
return


PossibleResolution:
call Section 'POSSIBLE RESOLUTION'

if rootCount = 0 then do
  call Emit 'No ENQ-specific recovery is recommended from this run.'
  return
end

call Emit '1. Resolve the condition affecting the terminal holder first.'
call Emit '2. If it has a valid WTOR, follow the exact message/runbook'
call Emit '   instructions and confirm application ownership.'
call Emit '3. If a low-priority batch holder is confirmed stalled, use the'
call Emit '   site-approved cancellation procedure only after validation.'
call Emit '4. If the holder is a production STC/application region, involve'
call Emit '   the owning team before disruptive recovery unless emergency'
call Emit '   procedures explicitly authorize otherwise.'
call Emit '5. Do not manually DEQ a resource merely to clear the symptom.'
call Emit ''
call Emit 'ENQDIAG does not execute CANCEL, FORCE, REPLY, DEQ or MODIFY.'
return


CommandReference:
call Section 'COMMAND REFERENCE'

call Emit 'Primary diagnostic command used by this run:'
call Emit '  'ShortText(grsCommand,124)
call Emit ''

if mode = 'JOB' then do
  if targetSys <> '' then do
    call Emit 'Useful read-only follow-up commands:'
    call Emit '  D GRS,ANALYZE,DEPENDENCY,SYS='targetSys',JOB='targetJob','
    call Emit '    COUNT='analysisCount',DETAIL'
    call Emit '  RO 'targetSys',D A,'targetJob
    call Emit '  RO 'targetSys',D R,R'
  end
end
else if mode = 'DSN' then do
  call Emit 'Useful read-only follow-up command:'
  call Emit '  D GRS,RES=(SYSDSN,'targetDsn')'
end
else do
  call Emit 'Useful read-only follow-up command:'
  call Emit '  D GRS,ANALYZE,DEPENDENCY,COUNT='analysisCount',DETAIL'
end

call Emit ''
call Emit 'Recovery / MODIFY commands:'
call Emit '  No generic MODIFY command is generated for ENQ contention.'
call Emit '  Any F, REPLY, CANCEL or FORCE action is product/message/site'
call Emit '  specific and must be validated before it is entered.'
return


Recommendations:
call Section 'RECOMMENDED DEBUGGING'

if rootCount = 0 then do
  call Emit '1. No current GRS dependency was identified by this run.'
  call Emit '2. If the job remains delayed, continue with non-GRS wait'
  call Emit '   analysis such as WTOR, allocation, I/O or product waits.'
  return
end

call Emit '1. Start with the highest-impact terminal holder above.'
call Emit '2. A terminal holder means GRS cannot continue that dependency'
call Emit '   path. It does NOT by itself prove the holder is hung.'
call Emit '3. Give high weight to an outstanding WTOR or explicit joblog'
call Emit '   evidence. Review the exact message before taking action.'
call Emit '4. If CPU/EXCP progress is observed, do not treat long elapsed'
call Emit '   time alone as a reason to cancel the holder.'
call Emit '5. If no progress is observed, the wait is long, and the holder'
call Emit '   blocks important work, investigate the holder first.'
call Emit '6. When JES spool is inaccessible, use the system/job/ASID from'
call Emit '   this report and access that JES environment if joblog detail'
call Emit '   is still required.'
call Emit '7. Cancellation/FORCE/REPLY remains a human/site-procedure'
call Emit '   decision. ENQDIAG intentionally does not automate recovery.'
return


Notes:
call Section 'DATA SOURCES / LIMITATIONS'
call Emit '  * ENQDIAG is read-only.'
call Emit '  * Primary dependency discovery uses D GRS,ANALYZE,DEPENDENCY.'
call Emit '  * Separate JES spool does not stop GRS dependency tracing.'
call Emit '  * DA/SR/JES visibility depends on SDSF scope and authority.'
call Emit '  * Remote ROUTE fallbacks issue display commands only.'
call Emit '  * No CPU/EXCP movement during a short sample is evidence, not'
call Emit '    proof, that an address space is hung.'
call Emit '  * Stalled-holder assessments use ENQDIAG triage heuristics;'
call Emit '    they are not IBM-defined severity thresholds.'
call Emit '  * GRS wait and address-space elapsed time are different facts.'
call Emit '  * A terminal holder may be waiting on something not managed by'
call Emit '    GRS; joblog/product-specific investigation may still be needed.'
return


Footer:
call Emit ''
call Emit copies('=',78)
call Emit ' ENQDIAG completed. No system or workload changes were made.'
call Emit copies('=',78)
return


SampleReport:
call Emit ''
call Emit copies('=',78)
call Emit ' ENQDIAG - ENQ / GRS DEPENDENCY DIAGNOSTIC'
call KV 'Author', 'SARATH S'
call KV 'Date', DisplayDate()
call KV 'Time', time('N')
call KV 'Executed from system', localSys
call KV 'Report data set', sampleDsn
call KV 'Mode', 'SAMPLE'
call KV 'Data source', 'SAMPLE DATA - NO SDSF/GRS COMMANDS WERE ISSUED'
call Emit copies('=',78)

call Section 'EXECUTIVE SUMMARY'
call KV 'Overall assessment', 'HIGH - SAMPLE'
call KV 'Reported job', 'PAYJOB1'
call KV 'Reported system', localSys
call KV 'Oldest GRS wait', '06:17:22'
call KV 'Dependency depth', '2'
call KV 'Terminal holder', 'BATCHX/00D2'
call KV 'Upstream units affected', '3'
call KV 'Deadlock detected', 'NO'
call Emit ''
call Emit 'SAMPLE INTERPRETATION: PAYJOB1 is waiting for a SYSDSN'
call Emit 'resource. The first holder is itself waiting for another'
call Emit 'SYSDSN resource. BATCHX is the terminal holder.'

call Section 'PRIMARY FINDINGS'
call Emit 'Pri Waiter/ASID       System   Wait       Res  Terminal holder'
call Emit copies('-',78)
call Emit '   1 PAYJOB1/006A      'left(localSys,8),
call Emit '     06:17:22     2 BATCHX/00D2'
call Emit ''
call Emit 'The terminal holder is the first address space to investigate.'
call Emit 'Long runtime alone does not prove that BATCHX is hung.'

call Section 'SYSTEM / TARGET SNAPSHOT'
call KV 'System name', localSys
call KV 'Sample waiter', 'PAYJOB1 / ASID 006A'
call KV 'Sample intermediate holder', 'LOADJOB / ASID 0081'
call KV 'Sample terminal holder', 'BATCHX / ASID 00D2'
call KV 'Sample terminal elapsed', '001:03:42:10'
call KV 'Sample CPU delta', '0.00 sec over 5 seconds'
call KV 'Sample EXCP delta', '0'
call KV 'Sample outstanding WTOR', 'YES'
call KV 'Sample JES spool', 'ACCESSIBLE'

call Section 'DETAILED ANALYSIS - DEPENDENCY CHAINS'
call Emit ''
call Emit '[1] 'localSys' / PAYJOB1 / ASID=006A'
call Emit '  -> [SYSDSN] PAY.DATA.INPUT'
call Emit '       wait=06:17:22'
call Emit '      HOLDER: 'localSys' / LOADJOB / ASID=0081'
call Emit '    -> [SYSDSN] PAY.DATA.WORK'
call Emit '         wait=05:58:41'
call Emit '        HOLDER: 'localSys' / BATCHX / ASID=00D2'
call Emit '        GRS RESULT: THIS UNIT OF WORK IS NOT WAITING'

call Section 'DETAILED ANALYSIS - TERMINAL HOLDERS'
call Emit 'Rank Terminal holder        System  Impact Direct Oldest wait'
call Emit copies('-',78)
call Emit '   1 BATCHX/00D2           'left(localSys,8),
call Emit '          3      1 06:17:22'
call Emit ''
call Emit 'Impact is the number of upstream units represented by this'
call Emit 'sample dependency graph.'

call Section 'SUPPORTING EVIDENCE'
call KV 'GRS role', 'TERMINAL HOLDER - SAMPLE'
call KV 'Address-space elapsed', '001:03:42:10'
call KV 'Observed activity', 'NO CPU OR EXCP PROGRESS OBSERVED - SAMPLE'
call KV 'Outstanding SR messages', '1 - SAMPLE'
call KV 'JES spool evidence', 'ACCESSIBLE - SAMPLE'
call Emit '  SAMPLE SR: Operator/application action is pending.'
call Emit '  SAMPLE LOG: Relevant holder message would be shown here.'

call Section 'COMMAND EVIDENCE'
call Emit 'The following are real read-only command patterns that ENQDIAG'
call Emit 'uses or may recommend. SAMPLE mode does NOT issue them.'
call Emit ''
call Emit '  D GRS,ANALYZE,DEPENDENCY,SYS='localSys',JOB=PAYJOB1,'
call Emit '    COUNT=10,DETAIL'
call Emit '  D GRS,RES=(SYSDSN,PAY.DATA.INPUT)'
call Emit '  RO 'localSys',D A,PAYJOB1'
call Emit '  RO 'localSys',D A,BATCHX'
call Emit '  RO 'localSys',D R,R'

call Section 'INTERPRETATION'
call Emit 'BATCHX is the terminal GRS holder in this sample chain.'
call Emit 'The sample also shows no CPU/EXCP movement and an outstanding'
call Emit 'operator request, so it would receive high investigation'
call Emit 'priority. Those facts still do not authorize automatic cancel.'

call Section 'RECOMMENDED DEBUGGING'
call Emit '1. Review the exact terminal-holder WTOR or application message.'
call Emit '2. Verify whether BATCHX is expected to be active this long.'
call Emit '3. Confirm the holder is making progress before recovery.'
call Emit '4. If spool is inaccessible, use GRS plus routed DISPLAY'
call Emit '   commands and then access the remote JES environment if needed.'
call Emit '5. Re-run dependency analysis after approved recovery to confirm'
call Emit '   that the upstream waiters have cleared.'

call Section 'POSSIBLE RESOLUTION'
call Emit 'SAMPLE ONLY: If the terminal holder is confirmed stalled, use'
call Emit 'the site-approved recovery for that job/application.'
call Emit ''
call Emit 'No executable CANCEL/REPLY/FORCE/MODIFY command is fabricated'
call Emit 'in SAMPLE mode because the correct action depends on the exact'
call Emit 'job type, product message, ownership and site procedure.'

call Section 'COMMAND REFERENCE'
call Emit 'Read-only diagnostic commands:'
call Emit '  D GRS,ANALYZE,DEPENDENCY,SYS='localSys',JOB=PAYJOB1,'
call Emit '    COUNT=10,DETAIL'
call Emit '  D GRS,RES=(SYSDSN,PAY.DATA.INPUT)'
call Emit '  RO 'localSys',D A,BATCHX'
call Emit '  RO 'localSys',D R,R'
call Emit ''
call Emit 'Recovery / MODIFY commands:'
call Emit '  INCIDENT-SPECIFIC - NOT GENERATED FROM SAMPLE DATA.'
call Emit '  Use only the exact product/runbook/message-directed action.'

call Section 'DATA SOURCES / LIMITATIONS'
call Emit '  * Everything describing PAYJOB1/LOADJOB/BATCHX is sample data.'
call Emit '  * The system name above is the real system running ENQDIAG.'
call Emit '  * SAMPLE mode does not initialize SDSF or issue GRS commands.'
call Emit '  * SAMPLE mode does not read JES spool or remote systems.'
call Emit '  * Display command syntax is shown for training/reference.'
call Emit '  * Separate JES spool does not prevent GRS dependency tracing.'
call Emit '  * A terminal holder may still be waiting on a non-GRS cause.'

call Emit ''
call Emit copies('=',78)
call Emit ' ENQDIAG SAMPLE completed. No system commands were issued.'
call Emit copies('=',78)
return


HelpReport:
call Emit ''
call Emit copies('=',78)
call Emit ' ENQDIAG - README / HELP'
call Emit copies('=',78)
call Emit ''
call Emit 'NAME'
call Emit '  ENQDIAG - z/OS ENQ / GRS dependency diagnostic utility.'
call Emit ''
call Emit 'PURPOSE'
call Emit '  ENQDIAG is a read-only first-response utility for diagnosing'
call Emit '  jobs or address spaces delayed by ENQ/resource contention.'
call Emit '  It follows GRS dependency chains from waiter to holder until'
call Emit '  it reaches the terminal holder that should be investigated.'
call Emit ''
call Emit 'WHEN TO USE'
call Emit '  * A job is running long and shows WAITING FOR DATASET.'
call Emit '  * You need to identify which job owns a required data set.'
call Emit '  * One holder is itself waiting for a resource held elsewhere.'
call Emit '  * The dependency crosses systems in the same sysplex.'
call Emit '  * You want a sysplex-wide view of important GRS waiters.'
call Emit ''
call Emit 'STANDARD REPORT STRUCTURE'
call Emit '  ENQDIAG live and SAMPLE reports use the same major sections:'
call Emit '  Executive Summary, Primary Findings, Detailed Analysis,'
call Emit '  Supporting Evidence, Interpretation, Recommended Debugging,'
call Emit '  Possible Resolution, Command Reference and Limitations.'
call Emit ''
call Emit 'WHAT ENQDIAG DOES'
call Emit '  1. Runs D GRS,ANALYZE,DEPENDENCY as the primary analysis.'
call Emit '  2. Builds waiter -> resource -> holder dependency chains.'
call Emit '  3. Reports parsed GRS wait duration for each dependency.'
call Emit '  4. Identifies terminal holders and downstream impact.'
call Emit '  5. Uses SDSF DA to obtain available runtime/activity data.'
call Emit '  6. Checks SDSF SR for outstanding operator requests.'
call Emit '  7. Reads useful JES evidence when the spool is accessible.'
call Emit '  8. Uses read-only routed DISPLAY commands when needed.'
call Emit ''
call Emit 'IMPORTANT'
call Emit '  A terminal GRS holder is not automatically a bad or hung job.'
call Emit '  ENQDIAG correlates available evidence before assigning a'
call Emit '  triage assessment. Recovery remains a human decision.'
call Emit ''
call Emit 'SYNTAX'
call Emit '  TSO ENQDIAG'
call Emit '  TSO ENQDIAG ALL'
call Emit '  TSO ENQDIAG JOB jobname'
call Emit '  TSO ENQDIAG JOB jobname SYS sysname'
call Emit '  TSO ENQDIAG JOB jobname JOBID jobid'
call Emit '  TSO ENQDIAG JOB jobname SYS sysname ASID asid'
call Emit '  TSO ENQDIAG DSN dataset.name'
call Emit '  TSO ENQDIAG SAMPLE'
call Emit '  TSO ENQDIAG HELP'
call Emit ''
call Emit 'CONVENIENCE'
call Emit '  TSO ENQDIAG jobname'
call Emit '  is treated as TSO ENQDIAG JOB jobname.'
call Emit ''
call Emit 'MODES'
call Emit '  ENQDIAG / ALL'
call Emit '      Sysplex-wide long-waiter and dependency health scan.'
call Emit ''
call Emit '  JOB'
call Emit '      Traces the dependency chain for a specified job.'
call Emit ''
call Emit '  DSN'
call Emit '      Starts analysis from a specified SYSDSN resource.'
call Emit ''
call Emit '  SAMPLE'
call Emit '      Creates a demonstration report using clearly marked'
call Emit '      sample incident data. It uses the real local system name'
call Emit '      but does not issue SDSF, GRS or remote commands.'
call Emit ''
call Emit 'OPTIONAL TUNING'
call Emit '  COUNT n'
call Emit '      Maximum GRS dependency requests. Default 10; max 99.'
call Emit ''
call Emit '  SAMPLES n'
call Emit '      DA activity samples for selected terminal holders.'
call Emit '      Default 2; maximum 5.'
call Emit ''
call Emit '  INTERVAL n'
call Emit '      Seconds between DA samples. Default 5; range 2-30.'
call Emit ''
call Emit 'OUTPUT DATA SETS'
call Emit '  Diagnostic report:'
call Emit '      'reportDsn
call Emit ''
call Emit '  SAMPLE REPORT:'
call Emit '      'sampleDsn
call Emit ''
call Emit '  HELP / README:'
call Emit '      'helpDsn
call Emit ''
call Emit '  The diagnostic report remains cataloged after Browse ends.'
call Emit '  A later ENQDIAG diagnostic run replaces the previous RPT.'
call Emit '  SAMPLE and HELP do not overwrite the diagnostic RPT.'
call Emit ''
call Emit 'REMOTE SYSTEM / SPOOL BEHAVIOR'
call Emit '  GRS dependency analysis can identify work on another member'
call Emit '  even when that system uses a separate JES spool.'
call Emit '  If remote spool output is not visible, ENQDIAG continues'
call Emit '  with GRS, SDSF and read-only remote DISPLAY evidence.'
call Emit '  The report states when JES joblog evidence is unavailable.'
call Emit ''
call Emit 'SAFETY'
call Emit '  * Read-only diagnostic utility.'
call Emit '  * Does not issue CANCEL, FORCE, DEQ, SETGRS or REPLY.'
call Emit '  * Remote fallbacks use DISPLAY commands only.'
call Emit '  * Does not continuously monitor or repeatedly loop forever.'
call Emit '  * Use normal site authorization/change procedures for any'
call Emit '    recovery action suggested by the report.'
call Emit ''
call Emit 'LIMITATIONS'
call Emit '  * Results depend on SDSF, console and OPERCMDS authority.'
call Emit '  * Separate JES spool may prevent automatic joblog reading.'
call Emit '  * A terminal holder can be waiting on a non-GRS condition.'
call Emit '  * Long elapsed time alone is not proof that a job is hung.'
call Emit ''
call Emit copies('=',78)
return

/*====================================================================*/
/* HELPERS                                                            */
/*====================================================================*/

Section:
parse arg scTitle
call Emit ''
call Emit copies('-',78)
call Emit ' 'scTitle
call Emit copies('-',78)
return


KV:
parse arg kvLabel, kvValue
call Emit ' 'left(kvLabel,31)':' kvValue
return


Emit:
parse arg emText
if length(emText) > 132 then emText = left(emText,132)
rptCount = rptCount + 1
rpt.rptCount = emText
rpt.0 = rptCount
return


ValueOr:
parse arg voVal, voDefault
voVal = strip(voVal)
if voVal = '' then return voDefault
return voVal


YesNo:
parse arg ynVal
if ynVal then return 'YES'
return 'NO'


NodeLabel:
parse arg nlNode
nl = nodeJob.nlNode' / 'nodeSys.nlNode
if nodeAsid.nlNode <> '' then nl = nl' / ASID 'nodeAsid.nlNode
if nodeTcb.nlNode <> '' then nl = nl' / TCB 'nodeTcb.nlNode
return nl


GetVar:
parse arg gvStem, gvRow, gvDefault
gvName = gvStem'.'gvRow
if symbol(gvName) <> 'VAR' then return gvDefault
gvVal = value(gvName)
if strip(gvVal) = '' then return gvDefault
return strip(gvVal)


GetNested:
parse arg gnStem, gnTail, gnDefault
gnName = gnStem'.'gnTail
if symbol(gnName) <> 'VAR' then return gnDefault
return value(gnName)


AddUnique:
parse arg auStem, auCount, auValue
auValue = strip(auValue)
if auValue = '' then return auCount

 do au = 1 to auCount
  auName = auStem||au
  if value(auName) = auValue then return auCount
 end

auCount = auCount + 1
auName = auStem||auCount
auOld = value(auName,auValue)
return auCount


StripHex:
parse arg shVal
shVal = translate(strip(shVal))
if left(shVal,2) = "X'" then shVal = substr(shVal,3)
if right(shVal,1) = "'" then shVal = left(shVal,length(shVal)-1)
if left(shVal,2) = '0X' then shVal = substr(shVal,3)
return shVal


WaitToSeconds:
parse arg wt
wt = strip(wt)
if wt = '' then return -1
if pos('*',wt) > 0 then return -1

parse var wt wh ':' wm ':' ws
if datatype(wh,'W') = 0 then return -1
if datatype(wm,'W') = 0 then return -1
if datatype(ws,'W') = 0 then return -1
return (wh*3600) + (wm*60) + ws


DurationToSeconds:
parse arg ds
/* DA elapsed is normally ddd:hh:mm:ss. */
ds = strip(ds)
if ds = '' then return -1
if pos('*',ds) > 0 then return -1

parts = 0
rest = ds
 do while pos(':',rest) > 0
  parts = parts + 1
  p.parts = substr(rest,1,pos(':',rest)-1)
  rest = substr(rest,pos(':',rest)+1)
 end
parts = parts + 1
p.parts = rest

if parts = 4 then do
  do di = 1 to 4
    if datatype(p.di,'W') = 0 then return -1
  end
  return p.1*86400 + p.2*3600 + p.3*60 + p.4
end

if parts = 3 then return WaitToSeconds(ds)
return -1


SecondsToDuration:
parse arg sdSec
if sdSec < 0 then return 'UNKNOWN'

sdDays = sdSec % 86400
sdRem = sdSec // 86400
sdHr = sdRem % 3600
sdRem = sdRem // 3600
sdMin = sdRem % 60
sdS = sdRem // 60

if sdDays > 0 then
  return sdDays'd 'right(sdHr,2,'0')':'right(sdMin,2,'0')':' ,
         right(sdS,2,'0')

return right(sdHr,2,'0')':'right(sdMin,2,'0')':'right(sdS,2,'0')


ScaledNumber:
parse arg sn
sn = translate(strip(sn))
if sn = '' then return -1
sn = changestr(',','',sn)
mult = 1
last = right(sn,1)
if last = 'K' then do
  mult = 1000
  sn = left(sn,length(sn)-1)
end
else if last = 'M' then do
  mult = 1000000
  sn = left(sn,length(sn)-1)
end
else if last = 'G' then do
  mult = 1000000000
  sn = left(sn,length(sn)-1)
end

if datatype(sn,'N') = 0 then return -1
return sn * mult


FinalCpu:
parse arg fcNode
if sampleCount >= 3 & nodeCpu3.fcNode <> '' then return nodeCpu3.fcNode
if sampleCount >= 2 then return nodeCpu2.fcNode
return nodeCpu1.fcNode


FinalExcp:
parse arg fxNode
if sampleCount >= 3 & nodeExcp3.fxNode <> '' then return nodeExcp3.fxNode
if sampleCount >= 2 then return nodeExcp2.fxNode
return nodeExcp1.fxNode


ShortText:
parse arg stText, stLen
stText = strip(stText)
if length(stText) <= stLen then return stText
if stLen <= 3 then return left(stText,stLen)
return left(stText,stLen-3)'...'


DisplayDate:
d = date('S')
yyyy = left(d,4)
mm = substr(d,5,2)
dd = right(d,2)
mons = 'JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC'
mon = word(mons,mm+0)
return dd'-'mon'-'yyyy


SleepSeconds:
parse arg slSec
if ussOn = 0 then return
if slSec <= 0 then return
Address SYSCALL 'sleep' slSec
return


/*====================================================================*/
/* REPORT PRESENTATION                                                */
/*====================================================================*/

PresentReport:
/* ZDIAG BATCH BYPASS */
if sysvar('SYSISPF') <> 'ACTIVE' then do
  if rptCount = 0 then return
  address TSO 'EXECIO 'rptCount' DISKW ZREPORT (STEM rpt. FINIS)'
  zdbw = rc
  if zdbw <> 0 then do
    say 'ZDIAG BATCH: ZREPORT write failed RC='zdbw
    exit 20
  end
  return
end
if rptCount = 0 then return

if mode = 'HELP' then rptDsn = helpDsn
else if mode = 'SAMPLE' then rptDsn = sampleDsn
else rptDsn = reportDsn

ddn = 'EQRPT001'

/* Replace the prior copy at the start of a new run, but never delete */
/* the newly written report after Browse ends.                        */
zz = outtrap('eqQuiet.')
Address TSO "DELETE '"rptDsn"'"
zz = outtrap('OFF')

allocCmd = "ALLOC DA('"rptDsn"') F("ddn") NEW CATALOG"
allocCmd = allocCmd ' TRACKS SPACE(10 5) DSORG(PS)'
allocCmd = allocCmd ' RECFM(F B) LRECL(133) BLKSIZE(0)'
Address TSO allocCmd
prRc = rc

if prRc <> 0 then do
  say 'ENQDIAG: Unable to allocate output data set:'
  say "  '"rptDsn"'"
  do prI = 1 to rptCount
    say rpt.prI
  end
  return
end

Address TSO 'EXECIO 'rptCount' DISKW 'ddn' (STEM rpt. FINIS)'
prWrc = rc
Address TSO 'FREE F('ddn')'

if prWrc <> 0 then do
  say 'ENQDIAG: Unable to write output data set:'
  say "  '"rptDsn"'"
  do prI = 1 to rptCount
    say rpt.prI
  end
  return
end

if ispfActive = 0 then do
  say "ENQDIAG output saved in '"rptDsn"'"
  do prI = 1 to rptCount
    say rpt.prI
  end
  return
end

Address ISPEXEC 'CONTROL ERRORS RETURN'
Address ISPEXEC "BROWSE DATASET('"rptDsn"')"
prBrc = rc
Address ISPEXEC 'CONTROL ERRORS CANCEL'

/* Intentionally keep the data set after the user exits Browse. */
if prBrc > 4 then do
  say 'ENQDIAG: ISPF Browse failed RC='prBrc
  say "ENQDIAG report remains saved in '"rptDsn"'"
  do prI = 1 to rptCount
    say rpt.prI
  end
end
return

Cleanup:
if ussOn = 1 then do
  cc = syscalls('OFF')
  ussOn = 0
end

if sdsfOn = 1 then do
  cc = isfcalls('OFF')
  sdsfOn = 0
end
return


FatalSyntax:
say
a = condition('C')
b = condition('D')
say 'ENQDIAG: REXX syntax/runtime error.'
say 'Condition:' a
say 'Description:' b
say 'Source line:' sigl
if datatype(sigl,'W') then say 'Statement:' sourceline(sigl)
if symbol('SDSFON') <> 'LIT' then call Cleanup
exit 20
