/* REXX */
/*--------------------------------------------------------------------*/
/* ASIDDIAG - ASID shortage first-response diagnostic                 */
/*                                                                    */
/* Author  : SARATH S                                                 */
/* Purpose : Read-only triage of ASID shortage, non-reusable ASIDs,   */
/*           active address-space concentration and recent churn.     */
/*                                                                    */
/* Invocation examples:                                              */
/*   TSO ASIDDIAG                                                    */
/*   TSO ASIDDIAG 60                                                 */
/*   TSO ASIDDIAG 60 15                                              */
/*   TSO ASIDDIAG HELP                                               */
/*   TSO ASIDDIAG SAMPLE                                             */
/*                                                                    */
/* Output:                                                           */
/*   <userid>.ASIDDIAG.RPT    - latest live diagnostic report.        */
/*   <userid>.ASIDDIAG.HELP   - README / command reference.           */
/*   <userid>.ASIDDIAG.SAMPLE - fabricated demonstration report.      */
/*                                                                    */
/* Parameters:                                                       */
/*   1 - SYSLOG history in minutes. Default 60. Range 30-240.         */
/*   2 - Top rows in rankings. Default 10. Range 1-25.                */
/*                                                                    */
/* Design principles:                                                */
/* - Single member. No site-specific data sets or job names.          */
/* - Read-only. No START, STOP, CANCEL, SET or IPL command is issued. */
/* - Uses SDSF SYS for MaxASID, FreeASID and BadASID.                 */
/* - Uses SDSF DA ALL for the live address-space population.          */
/* - Uses current SYSLOG, restricted to the requested time window.    */
/* - SDSF CK / IEA_ASIDS is optional and best effort.                 */
/* - Lifecycle counts prefer the larger of JES2 and MVS counts so     */
/*   $HASP373/$HASP395 and IEF403I/IEF404I are not double-counted.    */
/* - Restart churn is correlation only; it is not proof that a task   */
/*   caused a particular ASID to become non-reusable.                 */
/* - Live, HELP and SAMPLE output use separate persistent data sets. */
/* - Under ISPF, reports open in Browse for F7/F8/F9 support.         */
/* - Runtime failures print the original stage/line without recursion. */
/*--------------------------------------------------------------------*/

numeric digits 20
signal on syntax name FatalSyntax

asidVersion = '1.4'
historyMin = 60
topN = 10

rpt. = ''
rpt.0 = 0
rptCount = 0
reportMode = 'REPORT'
diagStage = 'INITIALIZATION'
ispfActive = 0
sdsfOn = 0

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

localSys = translate(MVSVAR('SYSNAME'))

parse upper arg pHist pTop .

if pHist = 'HELP' | pHist = 'H' | pHist = '?' then do
  reportMode = 'HELP'
  call HelpReport
  call PresentReport
  exit 0
end

if pHist = 'SAMPLE' | pHist = 'S' then do
  reportMode = 'SAMPLE'
  call SampleReport
  call PresentReport
  exit 0
end

if pHist <> '' then historyMin = pHist
if pTop <> '' then topN = pTop

if datatype(historyMin,'W') = 0 then do
  call Emit 'ASIDDIAG: History value must be a whole number.'
  call PresentReport
  exit 12
end

if datatype(topN,'W') = 0 then do
  call Emit 'ASIDDIAG: Top-row value must be a whole number.'
  call PresentReport
  exit 12
end

if historyMin < 30 then historyMin = 30
if historyMin > 240 then historyMin = 240
if topN < 1 then topN = 10
if topN > 25 then topN = 25

runStartTime = time('N')
runStartSec = time('S')

/* Thresholds below are triage heuristics, not IBM thresholds. */
restartMin = 5
massStartMin = 20
highActiveMin = 20

/* SYS snapshot data. */
sysFetchCount = 0
firstFREEASID = 'N/A'
firstBADASID = 'N/A'
lastSYSLEVEL = 'N/A'
lastMAXASID = 'N/A'
lastFREEASID = 'N/A'
lastBADASID = 'N/A'
lastSTCNUM = 'N/A'
lastJOBNUM = 'N/A'
lastTSUNUM = 'N/A'
lastIPLDATE = 'N/A'
lastIPLDAYS = 'N/A'
lastSTATDATE = 'N/A'

/* Current DA population. */
daAvailable = 0
daRows = 0
actKeys.0 = 0
actCnt. = 0
actLabel. = ''
actType. = ''
actAsid. = ''
actSeen. = 0

/* Recent lifecycle population. */
evtKeys.0 = 0
evtSeen. = 0
evtLabel. = ''
evS5. = 0
evS15. = 0
evS30. = 0
evSW. = 0
evE5. = 0
evE15. = 0
evE30. = 0
evEW. = 0
evF5. = 0
evF15. = 0
evF30. = 0
evFW. = 0
evPattern. = 'NORMAL'

/* ASID / Health Checker message state. */
msg059 = 0
msg060 = 0
msg061 = 0
msg062 = 0
msgVEH020 = 0
msgVEH021 = 0
msgVEH060 = 0
msg352 = 0
age059 = 99999
age060 = 99999
age061 = 99999
age062 = 99999
ageVEH020 = 99999
ageVEH021 = 99999
ageVEH060 = 99999
age352 = 99999
asidMsg. = ''
asidMsgCount = 0

/* SYSLOG state. */
logAvailable = 1
logNote = ''
logStartDate = ''
logStartTime = ''
logStopDate = ''
logStopTime = ''
logMatchCap = 5000
logCapHit = 0
haspLifecycleCount = 0

/* Health Checker state. */
hcAvailable = 0
hcFound = 0
hcOwner = 'N/A'
hcState = 'N/A'
hcStatus = 'N/A'
hcResult = 'N/A'
hcSeverity = 'N/A'
hcExCount = 'N/A'
hcDate = 'N/A'

/* Suspect and clue state. */
primaryKey = ''
primaryJob = 'NONE IDENTIFIED'
primaryPattern = 'NONE'
primaryScore = 0
clue. = ''
clueCount = 0

/* Evaluation defaults. */
severity = 'LOW'
diagnosis = 'NO CURRENT ASID SHORTAGE CONFIRMED'
asidShortActive = 0
replShortActive = 0
freePct = 0
badPct = 0
freeDelta = 0
badDelta = 0

diagStage = 'SDSF INITIALIZATION'
rc = isfcalls('ON')
if rc <> 0 then do
  call Emit 'ASIDDIAG: Unable to establish SDSF REXX environment.'
  call Emit 'RC='rc
  call PresentReport
  exit 20
end
sdsfOn = 1

/* Do not inherit narrow interactive SDSF filters. */
isfprefix = '**'
isfowner = '*'
isfsysname = localSys

diagStage = 'INITIAL SDSF SYS SNAPSHOT'
call FetchSYS
if result <> 0 then do
  call Emit 'ASIDDIAG: SDSF SYS data could not be obtained.'
  call Cleanup
  call PresentReport
  exit 20
end

diagStage = 'SDSF DA ACTIVE ADDRESS SPACES'
call FetchDA
diagStage = 'SDSF CK / IEA_ASIDS'
call FetchHealth
diagStage = 'SYSLOG TIME RANGE PREPARATION'
call PrepareLogRange

/* Search only the requested SYSLOG interval. Failure is non-fatal. */
diagStage = 'SYSLOG $HASP373 START SCAN'
call ScanLifecycle '$HASP373', 'START', 'H'
if logAvailable then do
  diagStage = 'SYSLOG $HASP395 END SCAN'
  call ScanLifecycle '$HASP395', 'END', 'H'
end
if logAvailable & haspLifecycleCount = 0 then do
  diagStage = 'SYSLOG IEF403I START SCAN'
  call ScanLifecycle 'IEF403I', 'START', 'I'
end
if logAvailable & haspLifecycleCount = 0 then do
  diagStage = 'SYSLOG IEF404I END SCAN'
  call ScanLifecycle 'IEF404I', 'END', 'I'
end
if logAvailable then do
  diagStage = 'SYSLOG IEF450I FAILURE SCAN'
  call ScanLifecycle 'IEF450I', 'FAIL', 'I'
end

if logAvailable then do
  diagStage = 'SYSLOG IEA05 ASID MESSAGE SCAN'
  call ScanAsidPattern 'IEA05'
end
if logAvailable then do
  diagStage = 'SYSLOG IEA06 ASID MESSAGE SCAN'
  call ScanAsidPattern 'IEA06'
end
if logAvailable then do
  diagStage = 'SYSLOG IEAVEH0 ASID MESSAGE SCAN'
  call ScanAsidPattern 'IEAVEH0'
end
if logAvailable then do
  diagStage = 'SYSLOG IEF352I MESSAGE SCAN'
  call ScanAsidPattern 'IEF352I'
end

diagStage = 'EVENT CORRELATION'
call FinalizeEvents
call FindPrimarySuspect

if logAvailable & primaryKey <> '' then do
  if primaryPattern = 'FAIL / RESTART LOOP' |,
     primaryPattern = 'HIGH START/END CHURN' then
    call CollectClues primaryJob
end

/* A second SYS read provides a short-run direction, not a trend. */
diagStage = 'FINAL SDSF SYS SNAPSHOT'
call FetchSYS
diagStage = 'DIAGNOSTIC EVALUATION'
call Evaluate

diagStage = 'REPORT CONSTRUCTION'
call Header
call TriageSummary
call AsidStatus
call CurrentPopulation
call RecentActivity
call AsidEventReport
call HealthReport
call ClueReport
call Analysis
call Recommendations
call CommandReference
call Notes
call Footer
call Cleanup
diagStage = 'REPORT PRESENTATION'
call PresentReport
exit 0


/*====================================================================*/
/* SDSF DATA COLLECTION                                               */
/*====================================================================*/

FetchSYS:
drop SYSNAME. SYSLEVEL. MAXASID. FREEASID. BADASID.
drop STCNUM. JOBNUM. TSUNUM. IPLDATE. IPLDAYS. STATDATE.

isfcols = 'SYSNAME SYSLEVEL MAXASID FREEASID BADASID STCNUM' ,
          'JOBNUM TSUNUM IPLDATE IPLDAYS STATDATE'

Address SDSF "ISFEXEC SYS"
fsRc = rc

if fsRc <> 0 then do
  call Emit 'ASIDDIAG: SDSF SYS access failed. RC='fsRc
  call SdsfMessages
  return fsRc
end

rows = isfrows
if rows = 0 then return 8

pick = 0
do i = 1 to rows
  if translate(strip(SYSNAME.i)) = localSys then do
    pick = i
    leave
  end
end
if pick = 0 then pick = 1

if sysFetchCount = 0 then do
  firstFREEASID = ValueOr(FREEASID.pick,'N/A')
  firstBADASID = ValueOr(BADASID.pick,'N/A')
end

lastSYSLEVEL = ValueOr(SYSLEVEL.pick,'N/A')
lastMAXASID = ValueOr(MAXASID.pick,'N/A')
lastFREEASID = ValueOr(FREEASID.pick,'N/A')
lastBADASID = ValueOr(BADASID.pick,'N/A')
lastSTCNUM = ValueOr(STCNUM.pick,'N/A')
lastJOBNUM = ValueOr(JOBNUM.pick,'N/A')
lastTSUNUM = ValueOr(TSUNUM.pick,'N/A')
lastIPLDATE = ValueOr(IPLDATE.pick,'N/A')
lastIPLDAYS = ValueOr(IPLDAYS.pick,'N/A')
lastSTATDATE = ValueOr(STATDATE.pick,'N/A')
sysFetchCount = sysFetchCount + 1
return 0


FetchDA:
drop JNAME. JOBID. JTYPE. ASID.
isfcols = 'JNAME JOBID JTYPE ASID'
Address SDSF "ISFEXEC DA ALL"
fdRc = rc

if fdRc <> 0 then do
  daAvailable = 0
  return fdRc
end

daAvailable = 1
daRows = isfrows

do i = 1 to daRows
  jn = translate(strip(JNAME.i))
  if jn = '' then iterate

  key = 'K'c2x(jn)
  if actSeen.key = 0 then do
    actSeen.key = 1
    actKeys.0 = actKeys.0 + 1
    ak = actKeys.0
    actKeys.ak = key
    actLabel.key = jn
    actType.key = ValueOr(JTYPE.i,'N/A')
    actAsid.key = ValueOr(ASID.i,'N/A')
  end
  actCnt.key = actCnt.key + 1
end
return 0


FetchHealth:
drop NAME. OWNER. STATE. STATUS. RESULT. EXCOUNT.
drop SEVERITY.

isfcklim = 9999
isfcols = 'NAME OWNER STATE STATUS RESULT EXCOUNT SEVERITY'
Address SDSF "ISFEXEC CK"
fhRc = rc

if fhRc <> 0 then do
  hcAvailable = 0
  drop isfcklim
  return
end

hcAvailable = 1
rows = isfrows

do i = 1 to rows
  nm = translate(strip(NAME.i))
  if nm = 'IEA_ASIDS' then do
    hcFound = 1
    hcOwner = ValueOr(OWNER.i,'N/A')
    hcState = ValueOr(STATE.i,'N/A')
    hcStatus = ValueOr(STATUS.i,'N/A')
    hcResult = ValueOr(RESULT.i,'N/A')
    hcExCount = ValueOr(EXCOUNT.i,'N/A')
    hcSeverity = ValueOr(SEVERITY.i,'N/A')
    leave
  end
end

drop isfcklim
return


PrepareLogRange:
/* Maximum history is four hours, so at most one midnight is crossed. */
nowSec = time('S')
startSec = nowSec - (historyMin * 60)
dayBack = 0
if startSec < 0 then do
  startSec = startSec + 86400
  dayBack = 1
end

isfdate = 'mmddyyyy /'
baseDay = date('B') - dayBack
logStartDate = date('U',baseDay,'B')
logStopDate = date('U')
logStartTime = SecClock(startSec)
logStopTime = time('N')
return


ScanLifecycle:
parse arg slPattern, slKind, slSource
if \logAvailable then return

call InitLogSearch slPattern
matches = 0

do forever
  Address SDSF "ISFLOG READ TYPE(SYSLOG)"
  slRc = rc

  if slRc > 4 then do
    logAvailable = 0
    logNote = 'SYSLOG unavailable/not authorized; SDSF RC='slRc
    call ClearLogSearch
    return
  end

  lines = 0
  if symbol('ISFLINE.0') <> 'LIT' then lines = isfline.0

  if lines > 0 then do
    age = LogLineAge()

    do ix = 1 to lines
      ln = strip(isfline.ix)
      jn = ExtractJob(ln,slPattern)
      if jn <> '' then do
        call RecordLifecycle jn, slKind, slSource, age, ln
        matches = matches + 1
      end
    end
  end

  if matches >= logMatchCap then do
    logCapHit = 1
    leave
  end

  if symbol('ISFNEXTLINETOKEN') = 'LIT' then leave
  if isfnextlinetoken = '' then leave
  isfstartlinetoken = isfnextlinetoken
end

call ClearLogSearch
return


ScanAsidPattern:
parse arg saPattern
if \logAvailable then return

call InitLogSearch saPattern
matches = 0

do forever
  Address SDSF "ISFLOG READ TYPE(SYSLOG)"
  saRc = rc

  if saRc > 4 then do
    logAvailable = 0
    logNote = 'SYSLOG unavailable/not authorized; SDSF RC='saRc
    call ClearLogSearch
    return
  end

  lines = 0
  if symbol('ISFLINE.0') <> 'LIT' then lines = isfline.0

  if lines > 0 then do
    age = LogLineAge()

    do ix = 1 to lines
      ln = strip(isfline.ix)
      call RecordAsidMessage age, ln
      matches = matches + 1
    end
  end

  if matches >= logMatchCap then do
    logCapHit = 1
    leave
  end

  if symbol('ISFNEXTLINETOKEN') = 'LIT' then leave
  if isfnextlinetoken = '' then leave
  isfstartlinetoken = isfnextlinetoken
end

call ClearLogSearch
return


InitLogSearch:
parse arg ilPattern
drop isfstartlinetoken isfnextlinetoken isfline.
drop isffirstlinedate isffirstlinetime

isfsysid = localSys
isflogstartdate = logStartDate
isflogstarttime = logStartTime
isflogstopdate = logStopDate
isflogstoptime = logStopTime
isffind = ilPattern
isffindlim = 9999999
isfscrolltype = 'FINDNEXT'
isflinelim = 1
return


ClearLogSearch:
drop isffind isffindlim isfscrolltype isflinelim
drop isfstartlinetoken isfnextlinetoken isfline.
drop isffirstlinedate isffirstlinetime
return


/*====================================================================*/
/* EVENT PROCESSING                                                   */
/*====================================================================*/

RecordLifecycle:
parse arg rlJob, rlKind, rlSource, rlAge, rlLine
jn = translate(strip(rlJob))
if jn = '' then return
if rlSource = 'H' then haspLifecycleCount = haspLifecycleCount + 1

key = 'K'c2x(jn)
if evtSeen.key = 0 then do
  evtSeen.key = 1
  evtKeys.0 = evtKeys.0 + 1
  ek = evtKeys.0
  evtKeys.ek = key
  evtLabel.key = jn
end

select
  when rlKind = 'START' then code = 'S'
  when rlKind = 'END' then code = 'E'
  otherwise code = 'F'
end

base = rlSource || code
if rlAge <= 5 then call IncVar base'5.'key
if rlAge <= 15 then call IncVar base'15.'key
if rlAge <= 30 then call IncVar base'30.'key
if rlAge <= historyMin then call IncVar base'W.'key

/* A non-zero $HASP395 completion is also a failure indicator. */
if rlSource = 'H' & rlKind = 'END' then do
  if IsFailureEnd(rlLine) then do
    if rlAge <= 5 then call IncVar 'HF5.'key
    if rlAge <= 15 then call IncVar 'HF15.'key
    if rlAge <= 30 then call IncVar 'HF30.'key
    if rlAge <= historyMin then call IncVar 'HFW.'key
  end
end
return


FinalizeEvents:
do i = 1 to evtKeys.0
  key = evtKeys.i

  evS5.key = Max2(CountVar('HS5.'key),CountVar('IS5.'key))
  evS15.key = Max2(CountVar('HS15.'key),CountVar('IS15.'key))
  evS30.key = Max2(CountVar('HS30.'key),CountVar('IS30.'key))
  evSW.key = Max2(CountVar('HSW.'key),CountVar('ISW.'key))

  evE5.key = Max2(CountVar('HE5.'key),CountVar('IE5.'key))
  evE15.key = Max2(CountVar('HE15.'key),CountVar('IE15.'key))
  evE30.key = Max2(CountVar('HE30.'key),CountVar('IE30.'key))
  evEW.key = Max2(CountVar('HEW.'key),CountVar('IEW.'key))

  evF5.key = Max2(CountVar('HF5.'key),CountVar('IF5.'key))
  evF15.key = Max2(CountVar('HF15.'key),CountVar('IF15.'key))
  evF30.key = Max2(CountVar('HF30.'key),CountVar('IF30.'key))
  evFW.key = Max2(CountVar('HFW.'key),CountVar('IFW.'key))

  cur = actCnt.key
  starts = evSW.key
  ends = evEW.key
  fails = evFW.key

  if fails >= restartMin & starts >= restartMin then
    evPattern.key = 'FAIL / RESTART LOOP'
  else if starts >= massStartMin & cur >= highActiveMin &,
          starts > ends + 10 then
    evPattern.key = 'MASS START / ACTIVE BURST'
  else if starts >= massStartMin & ends * 100 >= starts * 70 then
    evPattern.key = 'HIGH START/END CHURN'
  else if cur >= highActiveMin then
    evPattern.key = 'HIGH ACTIVE COUNT'
  else evPattern.key = 'NORMAL'
end
return


FindPrimarySuspect:
primaryScore = 0
primaryKey = ''

/* First rank jobs that have recent lifecycle evidence. */
do i = 1 to evtKeys.0
  key = evtKeys.i
  pat = evPattern.key
  cur = actCnt.key

  select
    when pat = 'FAIL / RESTART LOOP' then score = 100000
    when pat = 'MASS START / ACTIVE BURST' then score = 80000
    when pat = 'HIGH START/END CHURN' then score = 60000
    when pat = 'HIGH ACTIVE COUNT' then score = 40000
    otherwise iterate
  end

  score = score + (evFW.key * 100) + (evSW.key * 5) + cur
  if score > primaryScore then do
    primaryScore = score
    primaryKey = key
  end
end

/* If no lifecycle suspect exists, fall back to a high live count. */
if primaryKey = '' then do
  do i = 1 to actKeys.0
    key = actKeys.i
    if actCnt.key >= highActiveMin & actCnt.key > primaryScore then do
      primaryScore = actCnt.key
      primaryKey = key
    end
  end
end

if primaryKey <> '' then do
  if evtLabel.primaryKey <> '' then primaryJob = evtLabel.primaryKey
  else primaryJob = actLabel.primaryKey

  if evtSeen.primaryKey then primaryPattern = evPattern.primaryKey
  else primaryPattern = 'HIGH ACTIVE COUNT'
end
return


CollectClues:
parse arg ccJob
if strip(ccJob) = '' | ccJob = 'NONE IDENTIFIED' then return
if \logAvailable then return

call InitLogSearch ccJob
matches = 0

do forever
  Address SDSF "ISFLOG READ TYPE(SYSLOG)"
  ccRc = rc
  if ccRc > 4 then do
    call ClearLogSearch
    return
  end

  lines = 0
  if symbol('ISFLINE.0') <> 'LIT' then lines = isfline.0

  if lines > 0 then do
    age = LogLineAge()
    do ix = 1 to lines
      ln = strip(isfline.ix)
      matches = matches + 1

      if clueCount < 10 & IsClue(ln) then do
        if \IsLifecycleLine(ln) then do
          clueCount = clueCount + 1
          clue.clueCount = AgeText(age)' 'substr(ln,1,112)
        end
      end
    end
  end

  if matches >= 750 then leave
  if symbol('ISFNEXTLINETOKEN') = 'LIT' then leave
  if isfnextlinetoken = '' then leave
  isfstartlinetoken = isfnextlinetoken
end

call ClearLogSearch
return


RecordAsidMessage:
parse arg ramAge, ramLine
u = translate(ramLine)
recognized = 0

if pos('IEA059E',u) > 0 then do
  msg059 = msg059 + 1
  if ramAge < age059 then age059 = ramAge
  recognized = 1
end
if pos('IEA060I',u) > 0 then do
  msg060 = msg060 + 1
  if ramAge < age060 then age060 = ramAge
  recognized = 1
end
if pos('IEA061E',u) > 0 then do
  msg061 = msg061 + 1
  if ramAge < age061 then age061 = ramAge
  recognized = 1
end
if pos('IEA062I',u) > 0 then do
  msg062 = msg062 + 1
  if ramAge < age062 then age062 = ramAge
  recognized = 1
end
if pos('IEAVEH020E',u) > 0 then do
  msgVEH020 = msgVEH020 + 1
  if ramAge < ageVEH020 then ageVEH020 = ramAge
  recognized = 1
end
if pos('IEAVEH021E',u) > 0 then do
  msgVEH021 = msgVEH021 + 1
  if ramAge < ageVEH021 then ageVEH021 = ramAge
  recognized = 1
end
if pos('IEAVEH060E',u) > 0 then do
  msgVEH060 = msgVEH060 + 1
  if ramAge < ageVEH060 then ageVEH060 = ramAge
  recognized = 1
end
if pos('IEF352I',u) > 0 then do
  msg352 = msg352 + 1
  if ramAge < age352 then age352 = ramAge
  recognized = 1
end

if recognized & asidMsgCount < 25 then do
  asidMsgCount = asidMsgCount + 1
  asidMsg.asidMsgCount = AgeText(ramAge)' 'substr(ramLine,1,112)
end
return


/*====================================================================*/
/* EVALUATION                                                         */
/*====================================================================*/

Evaluate:
maxA = Num(lastMAXASID)
freeA = Num(lastFREEASID)
badA = Num(lastBADASID)

if maxA > 0 then do
  freePct = freeA * 100 / maxA
  badPct = badA * 100 / maxA
end

if IsNum(firstFREEASID) & IsNum(lastFREEASID) then
  freeDelta = Num(lastFREEASID) - Num(firstFREEASID)
if IsNum(firstBADASID) & IsNum(lastBADASID) then
  badDelta = Num(lastBADASID) - Num(firstBADASID)

asidShortActive = 0
if msg059 > 0 then do
  if msg060 = 0 | age059 < age060 then asidShortActive = 1
end

replShortActive = 0
if msg061 > 0 then do
  if msg062 = 0 | age061 < age062 then replShortActive = 1
end

/* FreeASID percentages here are local triage bands. IBM messages win. */
if replShortActive | asidShortActive | (maxA > 0 & freePct <= 5) then
  severity = 'CRITICAL'
else if maxA > 0 & freePct <= 10 then severity = 'HIGH'
else if primaryPattern <> 'NONE' & primaryPattern <> 'NORMAL' then
  severity = 'MEDIUM'
else if badDelta > 0 then severity = 'MEDIUM'
else severity = 'LOW'

if replShortActive & primaryPattern = 'FAIL / RESTART LOOP' then
  diagnosis = 'REPLACEMENT-ASID PRESSURE WITH RESTART LOOP'
else if replShortActive & primaryPattern = 'HIGH START/END CHURN' then
  diagnosis = 'REPLACEMENT-ASID PRESSURE WITH ADDRESS-SPACE CHURN'
else if replShortActive then
  diagnosis = 'REPLACEMENT / NON-REUSABLE ASID SHORTAGE'
else if (asidShortActive | (maxA > 0 & freePct <= 10)) &,
        primaryPattern = 'MASS START / ACTIVE BURST' then
  diagnosis = 'ACTIVE ASID CONCURRENCY PRESSURE'
else if (asidShortActive | (maxA > 0 & freePct <= 10)) &,
        primaryPattern = 'HIGH ACTIVE COUNT' then
  diagnosis = 'ACTIVE ASID CONCURRENCY PRESSURE'
else if primaryPattern = 'FAIL / RESTART LOOP' & badDelta > 0 then
  diagnosis = 'RESTART LOOP WITH BADASID GROWTH'
else if primaryPattern = 'FAIL / RESTART LOOP' then
  diagnosis = 'RESTART LOOP - NON-REUSABLE ASID RISK'
else if primaryPattern = 'HIGH START/END CHURN' & badDelta > 0 then
  diagnosis = 'ADDRESS-SPACE CHURN WITH BADASID GROWTH'
else if asidShortActive | (maxA > 0 & freePct <= 10) then
  diagnosis = 'ASID CAPACITY PRESSURE - CAUSE NOT ISOLATED'
else if msg059 > 0 & msg060 > 0 & age060 < age059 then
  diagnosis = 'RECENT ASID SHORTAGE - CURRENTLY RELIEVED'
else if msg061 > 0 & msg062 > 0 & age062 < age061 then
  diagnosis = 'RECENT REPLACEMENT-ASID SHORTAGE - RELIEVED'
else diagnosis = 'NO CURRENT ASID SHORTAGE CONFIRMED'
return


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

Header:
call Emit ''
call Emit copies('=',78)
call Emit ' ASIDDIAG - ASID SHORTAGE FIRST-RESPONSE DIAGNOSTIC'
call KV 'Author', 'SARATH S'
call KV 'Version', asidVersion
call KV 'Report type', 'LIVE DIAGNOSTIC'
call KV 'Date', DisplayDate()
call KV 'Time', time('N')
call KV 'System', localSys
call Emit copies('=',78)
return


TriageSummary:
call Section 'TRIAGE SUMMARY'
call KV 'Severity', severity
call KV 'Primary diagnosis', diagnosis
call KV 'Likely suspect', primaryJob
call KV 'Suspect pattern', primaryPattern
call KV 'History window', historyMin 'minutes'

if logAvailable then call KV 'SYSLOG analysis', 'AVAILABLE'
else call KV 'SYSLOG analysis', 'UNAVAILABLE - 'logNote

if hcAvailable & hcFound then call KV 'IEA_ASIDS check', hcStatus
else if hcAvailable then call KV 'IEA_ASIDS check', 'NOT FOUND'
else call KV 'IEA_ASIDS check', 'UNAVAILABLE / NOT AUTHORIZED'
return


AsidStatus:
call Section 'ASID STATUS'
call KV 'Maximum ASIDs', lastMAXASID
call KV 'Free ASIDs', lastFREEASID
if Num(lastMAXASID) > 0 then call KV 'Free ASID %', FormatPct(freePct)
else call KV 'Free ASID %', 'N/A'
call KV 'Non-reusable ASIDs', lastBADASID
if Num(lastMAXASID) > 0 then
  call KV 'BadASID % of MaxASID', FormatPct(badPct)
else call KV 'BadASID % of MaxASID', 'N/A'

call KV 'Started tasks', lastSTCNUM
call KV 'Batch jobs', lastJOBNUM
call KV 'TSO users', lastTSUNUM
call KV 'IPL date', lastIPLDATE
call KV 'IPL age', lastIPLDAYS 'days'

call Emit ''
call KV 'FreeASID at first read', firstFREEASID
call KV 'FreeASID at final read', lastFREEASID
if IsNum(firstFREEASID) & IsNum(lastFREEASID) then
  call KV 'FreeASID short-run delta', SignedInt(freeDelta)
else call KV 'FreeASID short-run delta', 'N/A'

call KV 'BadASID at first read', firstBADASID
call KV 'BadASID at final read', lastBADASID
if IsNum(firstBADASID) & IsNum(lastBADASID) then
  call KV 'BadASID short-run delta', SignedInt(badDelta)
else call KV 'BadASID short-run delta', 'N/A'

call Emit ''
call Emit 'The first-to-final delta covers only this ASIDDIAG invocation.'
call Emit 'It is directional evidence, not a long-term depletion rate.'
return


CurrentPopulation:
call Section 'CURRENT ADDRESS-SPACE POPULATION'

if \daAvailable then do
  call Emit 'SDSF DA ALL was unavailable for this invocation.'
  return
end

call KV 'Visible DA rows', daRows
call Emit ''
call Emit 'Top repeated job names currently active:'
call Emit ''
hdr = left('JOBNAME',10) right('ACTIVE',8) '  ' left('TYPE',8)
call Emit hdr
call Emit copies('-',32)

used. = 0
shown = 0
do out = 1 to topN
  bestKey = ''
  bestVal = -1

  do i = 1 to actKeys.0
    key = actKeys.i
    if used.key then iterate
    if actCnt.key > bestVal then do
      bestVal = actCnt.key
      bestKey = key
    end
  end

  if bestKey = '' then leave
  used.bestKey = 1
  if bestVal <= 1 & shown > 0 then leave

  line = left(actLabel.bestKey,10) right(bestVal,8) '  ' ,
         left(actType.bestKey,8)
  call Emit line
  shown = shown + 1
end

if shown = 0 then call Emit 'No DA address spaces were visible.'
return


RecentActivity:
call Section 'RECENT ADDRESS-SPACE START / END ACTIVITY'

if \logAvailable then do
  call Emit 'SYSLOG activity analysis is unavailable.'
  return
end

call KV 'Start', logStartDate logStartTime
call KV 'Stop', logStopDate logStopTime
call Emit ''

if evtKeys.0 = 0 then do
  call Emit 'No matching lifecycle messages were found in the window.'
  return
end

hdr = left('JOBNAME',9) right('5M S/E',9) right('15M S/E',10) ,
      right('30M S/E',10) right(historyMin'M S/E',11) ,
      right('FAIL',6) right('ACTIVE',8) '  PATTERN'
call Emit hdr
call Emit copies('-',118)

used. = 0
shown = 0
do out = 1 to topN
  bestKey = ''
  bestScore = -1

  do i = 1 to evtKeys.0
    key = evtKeys.i
    if used.key then iterate
    score = evSW.key + evEW.key + (evFW.key * 3) + actCnt.key
    if score > bestScore then do
      bestScore = score
      bestKey = key
    end
  end

  if bestKey = '' then leave
  used.bestKey = 1
  shown = shown + 1

  p5 = evS5.bestKey'/'evE5.bestKey
  p15 = evS15.bestKey'/'evE15.bestKey
  p30 = evS30.bestKey'/'evE30.bestKey
  pw = evSW.bestKey'/'evEW.bestKey

  line = left(evtLabel.bestKey,9) right(p5,9) right(p15,10) ,
         right(p30,10) right(pw,11) right(evFW.bestKey,6) ,
         right(actCnt.bestKey,8) '  ' evPattern.bestKey
  call Emit line
end

call Emit ''
call Emit 'S/E = observed start/end lifecycle count.'
call Emit 'JES2 lifecycle is primary; MVS start/end is the fallback source.'
if logCapHit then
  call Emit 'WARNING: At least one SYSLOG pattern reached the safety cap.'
return


AsidEventReport:
call Section 'ASID SHORTAGE / HEALTH MESSAGES'

if \logAvailable then do
  call Emit 'SYSLOG message correlation is unavailable.'
  return
end

call KV 'IEA059E ASID shortage', MsgState(msg059,age059)
call KV 'IEA060I ASID relief', MsgState(msg060,age060)
call KV 'IEA061E replacement shortage', MsgState(msg061,age061)
call KV 'IEA062I replacement relief', MsgState(msg062,age062)
call KV 'IEAVEH020E available ASIDs', MsgState(msgVEH020,ageVEH020)
call KV 'IEAVEH021E replacement ASIDs', MsgState(msgVEH021,ageVEH021)
call KV 'IEAVEH060E depletion forecast', MsgState(msgVEH060,ageVEH060)
call KV 'IEF352I address-space end', MsgState(msg352,age352)

if asidMsgCount > 0 then do
  call Emit ''
  call Emit 'Recent matching messages:'
  do i = 1 to asidMsgCount
    call Emit '  'asidMsg.i
  end
end
return


HealthReport:
call Section 'IEA_ASIDS HEALTH CHECK'

if \hcAvailable then do
  call Emit 'SDSF CK was unavailable or not authorized.'
  call Emit 'ASIDDIAG continued without Health Checker data.'
  return
end

if \hcFound then do
  call Emit 'IEA_ASIDS was not present in the visible CK rows.'
  call Emit 'ASIDDIAG continued with SYS, DA and SYSLOG evidence.'
  return
end

call KV 'Owner', hcOwner
call KV 'State', hcState
call KV 'Status', hcStatus
call KV 'Result', hcResult
call KV 'Exception count', hcExCount
call KV 'Severity', hcSeverity
return


ClueReport:
if clueCount = 0 then return
call Section 'POSSIBLE FAILURE CLUES FOR 'primaryJob
call Emit 'These are nearby SYSLOG clues, not a proven causal chain.'
call Emit ''
do i = 1 to clueCount
  call Emit '  'clue.i
end
return


Analysis:
call Section 'INTERPRETATION / LIKELY CAUSE'

select
  when diagnosis = 'ACTIVE ASID CONCURRENCY PRESSURE' then do
    call Emit 'The current evidence fits a live concurrency shortage.'
    call Emit primaryJob 'has a high active count and a recent start burst.'
    call Emit 'If those address spaces end normally, FreeASID may recover.'
    call Emit 'BadASID should still be watched; concurrency and permanent'
    call Emit 'non-reusable depletion are different conditions.'
  end

  when diagnosis = 'REPLACEMENT-ASID PRESSURE WITH RESTART LOOP' then do
    call Emit 'A repeated fail/start cycle is present while replacement-'
    call Emit 'ASID shortage evidence is active. 'primaryJob 'is therefore'
    call Emit 'a strong operational suspect for continued deterioration.'
    call Emit 'This report does not claim each termination created a bad'
    call Emit 'ASID; it establishes correlation that needs containment.'
  end

  when diagnosis =,
    'REPLACEMENT-ASID PRESSURE WITH ADDRESS-SPACE CHURN' then do
    call Emit 'High address-space turnover is occurring while replacement'
    call Emit 'ASID shortage evidence is active. Continued churn can make'
    call Emit 'the condition worse if terminations become non-reusable.'
  end

  when diagnosis = 'REPLACEMENT / NON-REUSABLE ASID SHORTAGE' then do
    call Emit 'Replacement-ASID shortage evidence is active, but ASIDDIAG'
    call Emit 'did not isolate one dominant live or restart pattern.'
    call Emit 'Review IEA_ASIDS, recent subsystem terminations and RSVNONR.'
  end

  when diagnosis = 'RESTART LOOP WITH BADASID GROWTH' then do
    call Emit primaryJob 'is repeatedly failing/restarting and BadASID'
    call Emit 'increased during this invocation. That is strong correlation'
    call Emit 'and warrants stopping further automated recycle attempts.'
  end

  when diagnosis = 'RESTART LOOP - NON-REUSABLE ASID RISK' then do
    call Emit primaryJob 'is in a repeated failure/restart cycle.'
    call Emit 'BadASID did not demonstrably rise during this short run, so'
    call Emit 'ASIDDIAG does not label it the proven source of BadASIDs.'
  end

  when diagnosis = 'ADDRESS-SPACE CHURN WITH BADASID GROWTH' then do
    call Emit 'High start/end churn is present and BadASID increased during'
    call Emit 'the invocation. Investigate the dominant churn source first.'
  end

  when diagnosis = 'ASID CAPACITY PRESSURE - CAUSE NOT ISOLATED' then do
    call Emit 'FreeASID is low or an ASID-shortage message is active, but'
    call Emit 'no single burst/restart pattern explains the condition.'
    call Emit 'Review normal peak population and ASID configuration.'
  end

  when diagnosis = 'RECENT ASID SHORTAGE - CURRENTLY RELIEVED' then do
    call Emit 'SYSLOG shows an ASID shortage followed by a newer relief.'
    call Emit 'Current values should be compared with the incident period.'
  end

  when diagnosis =,
    'RECENT REPLACEMENT-ASID SHORTAGE - RELIEVED' then do
    call Emit 'SYSLOG shows replacement-ASID shortage followed by relief.'
    call Emit 'The problem is not currently classified as active.'
  end

  otherwise do
    call Emit 'Current evidence does not confirm an ASID shortage.'
    if primaryPattern <> 'NONE' & primaryPattern <> 'NORMAL' then do
      call Emit 'However, unusual address-space behavior was detected:'
      call Emit primaryJob '-' primaryPattern
    end
  end
end
return


Recommendations:
call Section 'RECOMMENDED DEBUGGING / POSSIBLE RESOLUTION'

select
  when primaryPattern = 'FAIL / RESTART LOOP' then do
    call Emit '1. Confirm whether automation is repeatedly restarting ' ,
              primaryJob'.'
    call Emit '2. If operationally approved, prevent further restart churn'
    call Emit '   while the underlying startup/termination failure is fixed.'
    call Emit '3. Review the failure clues above and the task-specific log.'
    call Emit '4. Rerun ASIDDIAG and verify whether BadASID stops growing.'
    call Emit '5. If IEA061E remains active, review IEA_ASIDS and RSVNONR.'
  end

  when primaryPattern = 'MASS START / ACTIVE BURST' |,
       primaryPattern = 'HIGH ACTIVE COUNT' then do
    call Emit '1. Confirm whether the high concurrent population of ' ,
              primaryJob 'is expected.'
    call Emit '2. Check whether the instances are completing normally.'
    call Emit '3. Rerun ASIDDIAG after the burst subsides and confirm that'
    call Emit '   FreeASID recovers and BadASID is not climbing.'
    call Emit '4. Do not cancel valid workload solely because it is listed'
    call Emit '   here; first confirm business and subsystem impact.'
  end

  when replShortActive then do
    call Emit '1. Review the IEA_ASIDS Health Check if available.'
    call Emit '2. Look for recently recycled cross-memory/subsystem tasks.'
    call Emit '3. Review MAXUSER, RSVSTRT and RSVNONR in the active setup.'
    call Emit '4. Stop avoidable restart churn before considering recovery.'
  end

  when asidShortActive |,
       (Num(lastMAXASID) > 0 & freePct <= 10) then do
    call Emit '1. Review the active address-space concentrations above.'
    call Emit '2. Compare current load with the normal peak workload.'
    call Emit '3. Review MAXUSER, RSVSTRT and RSVNONR configuration.'
    call Emit '4. Rerun ASIDDIAG to determine whether FreeASID is recovering.'
  end

  otherwise do
    call Emit '1. No immediate ASID recovery action is indicated.'
    call Emit '2. If the ticket was intermittent, rerun during recurrence.'
    call Emit '3. Preserve SYSLOG around the incident if later analysis is'
    call Emit '   required.'
  end
end

if replShortActive | badDelta > 0 then do
  call Emit ''
  call Emit 'IPL consideration:'
  call Emit 'Stopping the source of churn prevents further deterioration;'
  call Emit 'it does not automatically make already non-reusable ASIDs'
  call Emit 'reusable. An IPL should be considered only when remaining'
  call Emit 'usable ASID capacity cannot safely sustain workload, and only'
  call Emit 'through the site''s normal change/operations procedure.'
end
return


CommandReference:
call Section 'USEFUL COMMANDS / MANUAL CHECKS'
call Emit 'The following are operator/manual follow-up commands.'
call Emit 'ASIDDIAG does not issue any of them automatically.'
call Emit ''
call Emit 'Read-only verification:'
call Emit '  SDSF SYS'
call Emit '      Review MaxASID, FreeASID and BadASID.'
call Emit '  SDSF DA ALL'
call Emit '      Review the live address-space population.'
call Emit '  SDSF CK'
call Emit '      Locate the IBMSUP IEA_ASIDS Health Check.'
call Emit '  D A,L'
call Emit '      Display active address spaces from the MVS console.'
call Emit '  D IPLINFO'
call Emit '      Display current IPL information.'
call Emit '  F HZSPROC,DISPLAY,CHECK(IBMSUP,IEA_ASIDS),DETAIL'
call Emit '      Display detailed IEA_ASIDS Health Checker information.'
call Emit ''
call Emit 'Conditional containment - STATE CHANGING:'
call Emit '  P <started-task-name>'
call Emit '      Use only after a restart loop is proven and stopping'
call Emit '      the task is approved by the site procedure.'
call Emit '      Do not substitute the ranked suspect automatically.'
call Emit ''
call Emit 'Configuration review:'
call Emit '  Review the active IEASYSxx MAXUSER, RSVSTRT and RSVNONR.'
call Emit '  These are capacity/configuration considerations, not a command'
call Emit '  that makes already non-reusable ASIDs reusable.'
return


Notes:
call Section 'IMPORTANT NOTES'
call Emit '  * MaxASID, FreeASID and BadASID come from SDSF SYS.'
call Emit '  * DA ALL is used only for the currently visible population.'
call Emit '  * SYSLOG history is restricted to 30-240 minutes.'
call Emit '  * SYSLOG/DA/CK results are limited by SDSF/SAF visibility.'
call Emit '  * Start/end counts are evidence of churn, not proof that a'
call Emit '    specific termination created a non-reusable ASID.'
call Emit '  * FreeASID <=5%/10% bands are ASIDDIAG triage heuristics.'
call Emit '    IBM IEA059E/061E message state takes precedence.'
call Emit '  * ASIDDIAG issues no corrective system/workload commands.'
call Emit '  * Live report:   <userid>.ASIDDIAG.RPT'
call Emit '  * Sample report: <userid>.ASIDDIAG.SAMPLE'
call Emit '  * HELP / README: <userid>.ASIDDIAG.HELP'
return


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


/*====================================================================*/
/* HELP                                                               */
/*====================================================================*/

HelpReport:
call Emit ''
call Emit copies('=',78)
call Emit ' ASIDDIAG - README / HELP'
call Emit copies('=',78)
call Emit ''
call Emit 'WHAT IS ASIDDIAG?'
call Emit copies('-',78)
call Emit 'ASIDDIAG is a read-only first-response diagnostic utility for'
call Emit 'investigating z/OS ASID shortage and replacement-ASID pressure.'
call Emit 'Its purpose is to help answer not only whether ASIDs are low,'
call Emit 'but what recent workload behavior may be contributing to it.'
call Emit ''
call Emit 'WHAT DOES IT CHECK?'
call Emit copies('-',78)
call Emit '  1. SDSF SYS for MaxASID, FreeASID, BadASID and system counts.'
call Emit '  2. SDSF DA ALL for unusually high active address-space counts.'
call Emit '  3. Recent SYSLOG for starts, ends, failures and restart churn.'
call Emit '  4. IEA059E/060I/061E/062I and related ASID messages.'
call Emit '  5. IEA_ASIDS Health Checker data when SDSF CK is available.'
call Emit ''
call Emit 'WHAT CAN IT IDENTIFY?'
call Emit copies('-',78)
call Emit '  * A mass start burst consuming many ASIDs concurrently.'
call Emit '  * A task repeatedly failing, ending and being restarted.'
call Emit '  * High address-space churn with increasing BadASID values.'
call Emit '  * Replacement/non-reusable ASID pressure.'
call Emit '  * General ASID capacity pressure where no single task is found.'
call Emit ''
call Emit 'IMPORTANT'
call Emit copies('-',78)
call Emit 'ASIDDIAG correlates evidence. A restart loop is not by itself'
call Emit 'proof that each termination created a non-reusable ASID.'
call Emit 'The report should be used as first-response evidence for further'
call Emit 'system-programmer investigation and operational decisions.'
call Emit ''
call Emit 'SYNTAX'
call Emit copies('-',78)
call Emit '  TSO ASIDDIAG'
call Emit '  TSO ASIDDIAG historyMinutes'
call Emit '  TSO ASIDDIAG historyMinutes topRows'
call Emit '  TSO ASIDDIAG HELP'
call Emit '  TSO ASIDDIAG SAMPLE'
call Emit ''
call Emit 'PARAMETERS'
call Emit copies('-',78)
call KV 'historyMinutes', '30-240. Default 60.'
call KV 'topRows', '1-25. Default 10.'
call Emit ''
call Emit 'EXAMPLES'
call Emit copies('-',78)
call Emit '  TSO ASIDDIAG'
call Emit '      Analyze the previous 60 minutes; show 10 ranked rows.'
call Emit ''
call Emit '  TSO ASIDDIAG 30 15'
call Emit '      Analyze the previous 30 minutes; show 15 ranked rows.'
call Emit ''
call Emit '  TSO ASIDDIAG SAMPLE'
call Emit '      Build a demonstration report without reading live SDSF data.'
call Emit ''
call Emit 'SAMPLE REPORT'
call Emit copies('-',78)
call Emit 'SAMPLE demonstrates the report format and interpretation using'
call Emit 'fabricated ASID-shortage data. The system name, invocation date'
call Emit 'and command syntax remain real. SAMPLE does not perform the live'
call Emit 'SYS, DA, SYSLOG or Health Checker collection.'
call Emit ''
call Emit 'REPORT DATA SET'
call Emit copies('-',78)
call Emit 'Under ISPF, the report is written to:'
call Emit '  <userid>.ASIDDIAG.RPT'
call Emit ''
call Emit 'The data set remains cataloged after Browse is closed, so the'
call Emit 'report can be reopened later from ISPF 3.4 or another session.'
call Emit 'The next ASIDDIAG run replaces the previous report in this data'
call Emit 'set. Under ISPF the new report automatically opens in Browse.'
call Emit ''
call Emit 'TSO ASIDDIAG HELP is saved separately as:'
call Emit '  <userid>.ASIDDIAG.HELP'
call Emit ''
call Emit 'TSO ASIDDIAG SAMPLE is saved separately as:'
call Emit '  <userid>.ASIDDIAG.SAMPLE'
call Emit ''
call Emit 'HELP and SAMPLE therefore do not overwrite the live report.'
call Emit ''
call Emit 'SAFETY'
call Emit copies('-',78)
call Emit 'Live ASIDDIAG is read-only. It does not START, STOP, CANCEL,'
call Emit 'SET, FORCE or IPL anything. Guidance is advisory only.'
call Emit 'SAMPLE does not perform live SDSF/SYSLOG diagnostic reads.'
call Emit ''
call Emit 'DATA AVAILABILITY'
call Emit copies('-',78)
call Emit 'Results depend on the SDSF/SAF visibility available to the user.'
call Emit 'Missing DA, SYSLOG or CK access is reported where possible; the'
call Emit 'utility continues with the evidence that is available.'
return


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

SampleReport:
call Emit ''
call Emit copies('=',78)
call Emit ' ASIDDIAG - ASID SHORTAGE FIRST-RESPONSE DIAGNOSTIC'
call KV 'Author', 'SARATH S'
call KV 'Version', asidVersion
call KV 'Report type', 'SAMPLE - FABRICATED DIAGNOSTIC DATA'
call KV 'Date', DisplayDate()
call KV 'Time', time('N')
call KV 'System', localSys
call Emit copies('=',78)
call Emit ''
call Emit '*** SAMPLE REPORT - VALUES BELOW ARE NOT LIVE SYSTEM DATA ***'
call Emit 'Only the system name/date/time and documented command syntax'
call Emit 'represent the current invocation environment.'

call Section 'TRIAGE SUMMARY'
call KV 'Severity', 'CRITICAL (SAMPLE)'
call KV 'Primary diagnosis', ,
        'REPLACEMENT-ASID PRESSURE WITH RESTART LOOP'
call KV 'Likely suspect', 'RMF (SAMPLE)'
call KV 'Suspect pattern', 'FAIL / RESTART LOOP'
call KV 'History window', '60 minutes (SAMPLE)'
call KV 'SYSLOG analysis', 'SAMPLE DATA'
call KV 'IEA_ASIDS check', 'EXCEPTION (SAMPLE)'

call Section 'ASID STATUS'
call KV 'Maximum ASIDs', '1200 (SAMPLE)'
call KV 'Free ASIDs', '38 (SAMPLE)'
call KV 'Free ASID %', '3.17% (SAMPLE)'
call KV 'Non-reusable ASIDs', '412 (SAMPLE)'
call KV 'BadASID % of MaxASID', '34.33% (SAMPLE)'
call KV 'Started tasks', '168 (SAMPLE)'
call KV 'Batch jobs', '24 (SAMPLE)'
call KV 'TSO users', '11 (SAMPLE)'
call KV 'IPL date', '01-AUG-2026 (SAMPLE)'
call KV 'IPL age', '18 days (SAMPLE)'
call Emit ''
call KV 'FreeASID at first read', '67 (SAMPLE)'
call KV 'FreeASID at final read', '38 (SAMPLE)'
call KV 'FreeASID short-run delta', '-29 (SAMPLE)'
call KV 'BadASID at first read', '389 (SAMPLE)'
call KV 'BadASID at final read', '412 (SAMPLE)'
call KV 'BadASID short-run delta', '+23 (SAMPLE)'
call Emit ''
call Emit 'Interpretation: usable ASID headroom is critically low while'
call Emit 'the sample BadASID count is rising during repeated task churn.'

call Section 'CURRENT ADDRESS-SPACE POPULATION'
call KV 'Visible DA rows', '244 (SAMPLE)'
call Emit ''
call Emit 'Top repeated job names currently active:'
call Emit ''
call Emit left('JOBNAME',10) right('ACTIVE',8) '  ' left('TYPE',8)
call Emit copies('-',32)
call Emit left('BPXAS',10) right('34',8) '  ' left('STC',8)
call Emit left('RMF',10) right('1',8) '  ' left('STC',8)
call Emit left('CICSREG',10) right('4',8) '  ' left('STC',8)
call Emit left('BATCHA',10) right('3',8) '  ' left('JOB',8)

call Section 'RECENT ADDRESS-SPACE START / END ACTIVITY'
call KV 'Start', 'SAMPLE - previous 60 minutes'
call KV 'Stop', 'SAMPLE - current invocation time'
call Emit ''
call Emit left('JOBNAME',9) right('5M S/E',9) right('15M S/E',10) ,
          right('30M S/E',10) right('60M S/E',11) ,
          right('FAIL',6) right('ACTIVE',8) '  PATTERN'
call Emit copies('-',118)
call Emit left('RMF',9) right('16/16',9) right('49/48',10) ,
          right('94/94',10) right('188/187',11) ,
          right('184',6) right('1',8) '  FAIL / RESTART LOOP'
call Emit left('BPXAS',9) right('2/2',9) right('6/5',10) ,
          right('11/10',10) right('21/18',11) ,
          right('0',6) right('34',8) '  NORMAL'
call Emit left('CICSREG',9) right('0/0',9) right('0/0',10) ,
          right('0/0',10) right('0/0',11) ,
          right('0',6) right('4',8) '  NORMAL'
call Emit ''
call Emit 'S/E = sample observed start/end lifecycle count.'
call Emit 'The RMF row demonstrates what a rapid recycle loop can look like.'

call Section 'ASID SHORTAGE / HEALTH MESSAGES'
call KV 'IEA059E ASID shortage', '1 occurrence; latest [22m] (SAMPLE)'
call KV 'IEA060I ASID relief', 'NOT FOUND IN SAMPLE WINDOW'
call KV 'IEA061E replacement shortage', ,
        '1 occurrence; latest [8m] (SAMPLE)'
call KV 'IEA062I replacement relief', 'NOT FOUND IN SAMPLE WINDOW'
call KV 'IEAVEH020E available ASIDs', 'NOT FOUND IN SAMPLE WINDOW'
call KV 'IEAVEH021E replacement ASIDs', 'NOT FOUND IN SAMPLE WINDOW'
call KV 'IEAVEH060E depletion forecast', 'NOT FOUND IN SAMPLE WINDOW'
call KV 'IEF352I address-space end', '187 occurrences (SAMPLE)'
call Emit ''
call Emit 'Recent matching messages (illustrative only):'
call Emit '  [22m] IEA059E ASID SHORTAGE HAS BEEN DETECTED'
call Emit '  [08m] IEA061E REPLACEMENT ASID SHORTAGE HAS BEEN DETECTED'

call Section 'IEA_ASIDS HEALTH CHECK'
call KV 'Owner', 'IBMSUP'
call KV 'State', 'ACTIVE(ENABLED) (SAMPLE)'
call KV 'Status', 'EXCEPTION (SAMPLE)'
call KV 'Result', 'EXCEPTION (SAMPLE)'
call KV 'Exception count', '1 (SAMPLE)'
call KV 'Severity', 'HIGH (SAMPLE)'

call Section 'POSSIBLE FAILURE CLUES FOR RMF'
call Emit 'These are fabricated clues showing the type of correlation'
call Emit 'ASIDDIAG can surface around a repeatedly failing task.'
call Emit ''
call Emit '  SAMPLE: ICH408I security authorization failure near RMF start'
call Emit '  SAMPLE: RMF termination followed by another automation start'
call Emit '  SAMPLE: repeated sequence continues throughout the window'

call Section 'INTERPRETATION / LIKELY CAUSE'
call Emit 'The sample evidence fits replacement-ASID pressure occurring'
call Emit 'at the same time as a severe RMF fail/restart cycle.'
call Emit 'RMF is therefore the first operational suspect to investigate.'
call Emit ''
call Emit 'The restart loop is correlation, not proof that every RMF end'
call Emit 'created a non-reusable ASID. The rising sample BadASID count and'
call Emit 'replacement-shortage evidence make continued churn unsafe.'

call Section 'RECOMMENDED DEBUGGING / POSSIBLE RESOLUTION'
call Emit '1. Confirm that RMF is actually failing and being restarted.'
call Emit '2. Review the RMF joblog and nearby RACF/security messages.'
call Emit '3. Identify the automation rule responsible for the restart.'
call Emit '4. If operationally approved, stop further restart churn while'
call Emit '   the startup/security problem is corrected.'
call Emit '5. Rerun ASIDDIAG and verify FreeASID/BadASID direction.'
call Emit '6. Review IEA_ASIDS and remaining replacement-ASID capacity.'
call Emit '7. Consider IPL only if usable ASID capacity cannot safely'
call Emit '   sustain workload after the source of churn is contained.'

call Section 'USEFUL COMMANDS / MANUAL CHECKS'
call Emit 'These are real command forms; ASIDDIAG SAMPLE does not issue them.'
call Emit ''
call Emit 'Read-only verification:'
call Emit '  SDSF SYS'
call Emit '      Review MaxASID, FreeASID and BadASID.'
call Emit '  SDSF DA ALL'
call Emit '      Review the live address-space population.'
call Emit '  SDSF CK'
call Emit '      Locate the IBMSUP IEA_ASIDS Health Check.'
call Emit '  D A,L'
call Emit '      Display active address spaces from the MVS console.'
call Emit '  D IPLINFO'
call Emit '      Display current IPL information.'
call Emit '  F HZSPROC,DISPLAY,CHECK(IBMSUP,IEA_ASIDS),DETAIL'
call Emit '      Display detailed IEA_ASIDS Health Checker information.'
call Emit ''
call Emit 'Conditional containment - STATE CHANGING:'
call Emit '  P RMF'
call Emit '      SAMPLE example only. Use only if RMF is confirmed to be'
call Emit '      recycling and site operations approves stopping it.'
call Emit '      If automation immediately restarts it, the automation rule'
call Emit '      must be handled using the site-specific procedure.'
call Emit ''
call Emit 'Configuration review:'
call Emit '  Review the active IEASYSxx MAXUSER, RSVSTRT and RSVNONR.'
call Emit '  Do not treat a parameter change as an immediate recovery for'
call Emit '  ASIDs that are already non-reusable in the current IPL.'

call Section 'IMPORTANT NOTES'
call Emit '  * THIS IS A SAMPLE REPORT. Diagnostic values are fabricated.'
call Emit '  * System name/date/time come from the current invocation.'
call Emit '  * Commands shown are advisory and are never auto-issued.'
call Emit '  * A restart loop is not automatic proof of BadASID creation.'
call Emit '  * Live ASIDDIAG depends on the caller SDSF/SAF visibility.'
call Emit '  * Live report:   <userid>.ASIDDIAG.RPT'
call Emit '  * Sample report: <userid>.ASIDDIAG.SAMPLE'
call Emit '  * HELP / README: <userid>.ASIDDIAG.HELP'

call Emit ''
call Emit copies('=',78)
call Emit ' ASIDDIAG SAMPLE completed. No live SDSF/SYSLOG data was read.'
call Emit ' No system or workload changes were made.'
call Emit copies('=',78)
return


/*====================================================================*/
/* UTILITY ROUTINES                                                   */
/*====================================================================*/

ExtractJob:
procedure
parse upper arg ejLine, ejPattern
p = pos(ejPattern,ejLine)
if p = 0 then return ''
rest = strip(substr(ejLine,p + length(ejPattern)))
jn = word(rest,1)
do while jn <> '' & (right(jn,1) = ',' | right(jn,1) = ':')
  jn = left(jn,length(jn) - 1)
end
if jn = '' then return ''
if length(jn) > 8 then return ''
return jn


IsFailureEnd:
procedure
parse upper arg ifeLine
if pos('ABEND',ifeLine) > 0 then return 1
p = pos('RC=',ifeLine)
if p = 0 then return 0
rest = substr(ifeLine,p + 3)
rcv = word(rest,1)
do while rcv <> '' & (right(rcv,1) = ',' | right(rcv,1) = ')')
  rcv = left(rcv,length(rcv) - 1)
end
if rcv = '' then return 0
if rcv = '0000' | rcv = '0' then return 0
return 1


IsClue:
procedure
parse upper arg icLine
if pos('RACF',icLine) > 0 then return 1
if pos('ICH',icLine) > 0 then return 1
if pos('IRR',icLine) > 0 then return 1
if pos('ABEND',icLine) > 0 then return 1
if pos('ERROR',icLine) > 0 then return 1
if pos('FAILED',icLine) > 0 then return 1
if pos('FAILURE',icLine) > 0 then return 1
if pos('NOT AUTH',icLine) > 0 then return 1
if pos('SECURITY',icLine) > 0 then return 1
if pos('VIOLAT',icLine) > 0 then return 1
if pos('REASON=',icLine) > 0 then return 1
if pos('RETURN CODE',icLine) > 0 then return 1
return 0


IsLifecycleLine:
procedure
parse upper arg ilLine
if pos('$HASP373',ilLine) > 0 then return 1
if pos('$HASP395',ilLine) > 0 then return 1
if pos('IEF403I',ilLine) > 0 then return 1
if pos('IEF404I',ilLine) > 0 then return 1
return 0


LogLineAge:
/* Uses first-line metadata because FINDNEXT returns one line at a time. */
if symbol('ISFFIRSTLINETIME') = 'LIT' then return historyMin
lt = strip(isffirstlinetime)
if lt = '' then return historyMin
lineSec = ClockSec(lt)
if lineSec < 0 then return historyMin

sameDay = 1
if symbol('ISFFIRSTLINEDATE') <> 'LIT' then do
  ld = DateKey(isffirstlinedate)
  sd = DateKey(logStopDate)
  if ld <> '' & sd <> '' & ld <> sd then sameDay = 0
end

nowSec = time('S')
if sameDay then ageSec = nowSec - lineSec
else ageSec = nowSec + 86400 - lineSec
if ageSec < 0 then ageSec = 0
return ageSec / 60


DateKey:
/* Normalize MM/DD/YYYY and MM/DD/YY to MMDDYY for comparison. */
procedure
parse arg dkDate
dkDate = strip(dkDate)
dkOut = ''
do dkI = 1 to length(dkDate)
  dkC = substr(dkDate,dkI,1)
  if pos(dkC,'0123456789') > 0 then dkOut = dkOut || dkC
end
if length(dkOut) = 8 then do
  dkOut = substr(dkOut,1,4) || right(dkOut,2)
end
return dkOut


ClockSec:
procedure
parse arg csTime
csTime = strip(csTime)
parse var csTime hh ':' mm ':' ss
if pos('.',ss) > 0 then parse var ss ss '.' .
if \datatype(hh,'N') | \datatype(mm,'N') | \datatype(ss,'N') then
  return -1
return (hh * 3600) + (mm * 60) + ss


SecClock:
procedure
parse arg scSec
hh = trunc(scSec / 3600)
rem = scSec - (hh * 3600)
mm = trunc(rem / 60)
ss = trunc(rem - (mm * 60))
return right(hh,2,'0')':'right(mm,2,'0')':'right(ss,2,'0')


AgeText:
procedure
parse arg atAge
if \datatype(atAge,'N') then return '[time N/A]'
if atAge < 1 then return '[<1m]'
return '['strip(format(atAge,5,0))'m]'


MsgState:
procedure
parse arg msCount, msAge
if msCount = 0 then return 'NOT FOUND IN WINDOW'
return msCount 'occurrence(s); latest' AgeText(msAge)


IncVar:
parse arg ivName
v = value(ivName)
if \datatype(v,'N') then v = 0
oldVal = value(ivName,v + 1)
return


CountVar:
parse arg cvName
v = value(cvName)
if \datatype(v,'N') then return 0
return v


Max2:
procedure
parse arg a,b
if a >= b then return a
return b


Num:
procedure
parse arg n
n = strip(n)
if n = '' then return 0
n = space(translate(n,'  ','%,') ,0)
if datatype(n,'N') then return n
return 0


IsNum:
procedure
parse arg n
n = strip(n)
if n = '' then return 0
n = space(translate(n,'  ','%,') ,0)
return datatype(n,'N')


ValueOr:
procedure
parse arg v, d
v = strip(v)
if v = '' then return d
return v


FormatPct:
procedure
parse arg n
if \datatype(n,'N') then return 'N/A'
return strip(format(n,7,2))'%'


SignedInt:
procedure
parse arg n
if \datatype(n,'N') then return 'N/A'
if n > 0 then return '+'strip(format(n,10,0))
return strip(format(n,10,0))


DisplayDate:
procedure
/* Convert DATE(S) YYYYMMDD into DD-MMM-YYYY without locale reliance. */
d = date('S')
yyyy = substr(d,1,4)
mm = substr(d,5,2)
dd = substr(d,7,2)
months = 'JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC'
mon = word(months,mm + 0)
return dd'-'mon'-'yyyy


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


KV:
parse arg k, v
call Emit left(k,30) ':' v
return


Emit:
procedure expose rpt. rptCount
out = ''
do ei = 1 to arg()
  piece = arg(ei)
  if ei = 1 then out = piece
  else out = out piece
end
rptCount = rptCount + 1
rpt.rptCount = out
rpt.0 = rptCount
return


SdsfMessages:
if symbol('ISFMSG') <> 'LIT' then do
  if strip(isfmsg) <> '' then call Emit ' SDSF:' isfmsg
end
if symbol('ISFMSG2.0') <> 'LIT' then do
  do sm = 1 to isfmsg2.0
    call Emit ' SDSF:' isfmsg2.sm
  end
end
return


PresentReport:
procedure expose rpt. rptCount ispfActive reportMode
/* 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 \ispfActive then do
  do pi = 1 to rptCount
    say rpt.pi
  end
  return
end

uid = sysvar('SYSUID')
if reportMode = 'HELP' then rptDsn = uid'.ASIDDIAG.HELP'
else if reportMode = 'SAMPLE' then rptDsn = uid'.ASIDDIAG.SAMPLE'
else rptDsn = uid'.ASIDDIAG.RPT'
ddn = 'ADRPT001'

zz = outtrap('adQuiet.')
Address TSO "DELETE '"rptDsn"'"
zz = outtrap('OFF')

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

Address TSO allocCmd
aRc = rc
if aRc <> 0 then do
  do pi = 1 to rptCount
    say rpt.pi
  end
  return
end

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

if wRc <> 0 then do
  Address TSO "DELETE '"rptDsn"'"
  do pi = 1 to rptCount
    say rpt.pi
  end
  return
end

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

/* Keep the data set cataloged after Browse. The next invocation       */
/* replaces the same REPORT, HELP or SAMPLE data set with newest data. */

if bRc > 4 then do
  say 'ASIDDIAG: ISPF Browse failed RC='bRc
  do pi = 1 to rptCount
    say rpt.pi
  end
end
return


Cleanup:
drop isfdate isfsysid
drop isflogstartdate isflogstarttime
drop isflogstopdate isflogstoptime
call ClearLogSearch

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


FatalSyntax:
/* Do not call Emit/PresentReport here. The error could have occurred   */
/* inside one of those routines. Capture the original condition first. */
signal off syntax
errRc = rc
errCond = condition('C')
errDesc = condition('D')
errInfo = condition('I')
errLine = 'N/A'
if symbol('SIGL') = 'VAR' then errLine = sigl

say ''
say copies('*',72)
say 'ASIDDIAG: REXX syntax/runtime error.'
say 'Stage       :' diagStage
say 'Condition   :' errCond
if errDesc <> '' then say 'Description :' errDesc
if errInfo <> '' then say 'Information :' errInfo
say 'REXX RC     :' errRc
say 'Source line :' errLine

if datatype(errLine,'W') then do
  srcCount = sourceline()
  if srcCount > 0 then do
    if errLine >= 1 & errLine <= srcCount then
      say 'Statement   :' sourceline(errLine)
  end
end
say copies('*',72)

/* Best-effort SDSF shutdown only. Do not invoke general cleanup here. */
if symbol('SDSFON') = 'VAR' then do
  if sdsfOn = 1 then tmp = isfcalls('OFF')
end
exit 20
