/* REXX */
/*--------------------------------------------------------------------*/
/* JOBDIAG - JES2 resource triage utility                             */
/*                                                                    */
/* Version : 1.7                                                     */
/* Author  : SARATH S                                             */
/* Purpose : Read-only analysis of JES2 spool, job queue, JOE, BERT,  */
/*           held output and output-class accumulation through SDSF.  */
/*                                                                    */
/* Invocation examples:                                              */
/*   TSO JOBDIAG                                                     */
/*   TSO JOBDIAG ALL                                                 */
/*   TSO JOBDIAG SPOOL                                               */
/*   TSO JOBDIAG TG                                                  */
/*   TSO JOBDIAG JQE                                                 */
/*   TSO JOBDIAG JNUM                                                */
/*   TSO JOBDIAG JOE                                                 */
/*   TSO JOBDIAG BERT                                                */
/*   TSO JOBDIAG HELD                                                */
/*   TSO JOBDIAG HELD K                                              */
/*   TSO JOBDIAG OUTPUT                                              */
/*   TSO JOBDIAG OUTPUT K                                            */
/*   TSO JOBDIAG CLASS K                                             */
/*   TSO JOBDIAG JOB PAY*                                            */
/*   TSO JOBDIAG DETAIL JOB12345                                     */
/*   TSO JOBDIAG HELP                                                */
/*                                                                    */
/* Design principles:                                                */
/* - Single member. No site-specific data sets, classes or job names. */
/* - Read-only. No purge, cancel, release, hold or JES modification.  */
/* - Uses supported SDSF REXX interfaces.                            */
/* - Degrades when optional JES2 columns or SYSLOG access are absent. */
/* - Numeric-suffix family detection: ABCDEF20/ABCDEF30 -> ABCDEF##.  */
/* - Uses SDSF SYS Spool% for actual primary-JES spool utilization.   */
/* - Under ISPF, presents results in Browse for F7/F8/F9 support.     */
/*                                                                    */
/* IMPORTANT:                                                        */
/* The user's SDSF/SAF authorization determines which jobs/output     */
/* groups/log records are visible. Reported totals therefore mean     */
/* "visible to this invocation", not necessarily unrestricted JES2    */
/* checkpoint totals.                                                */
/*--------------------------------------------------------------------*/

signal on syntax name FatalSyntax

jiVersion = '1.7'
topN = 10
groupPctWarn = 5
familyPctWarn = 5
classShareWarn = 20
repeatWarn = 100

rpt. = ''
rpt.0 = 0
rptCount = 0
ispfActive = 0
sdsfOn = 0
if sysvar('SYSISPF') = 'ACTIVE' then ispfActive = 1

parse upper arg rawArg
rawArg = strip(rawArg)

if rawArg = '' then do
  mode = 'ALL'
  parm = ''
end
else parse var rawArg mode parm

mode = strip(mode)
parm = strip(parm)

if mode = '?' then mode = 'HELP'
if mode = 'TG' then mode = 'SPOOL'
if mode = 'OUT' then mode = 'OUTPUT'

valid = ' ALL SPOOL JQE JNUM JOE BERT HELD OUTPUT CLASS JOB DETAIL ' ,
        'SAMPLE HELP '
if pos(' 'mode' ', valid) = 0 then do
  call Emit 'JOBDIAG: Unknown option:' mode
  call Emit 'Use TSO JOBDIAG HELP'
  call PresentReport
  exit 12
end

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

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

if (mode = 'CLASS' | mode = 'JOB' | mode = 'DETAIL') & parm = '' then do
  call Emit 'JOBDIAG:' mode 'requires an argument.'
  call Emit 'Use TSO JOBDIAG HELP'
  call PresentReport
  exit 12
end

needST = 0
needH = 0
needO = 0
needSYS = 0
needJRI = 0

select
  when mode = 'ALL' then do
    needST = 1
    needH = 1
    needO = 1
    needSYS = 1
    needJRI = 1
  end
  when mode = 'SPOOL' then do
    needST = 1
    needSYS = 1
    needJRI = 1
  end
  when mode = 'HELD' then needH = 1
  when mode = 'OUTPUT' then needO = 1
  when mode = 'CLASS' then do
    needH = 1
    needO = 1
  end
  when mode = 'JQE' | mode = 'JNUM' | mode = 'JOE' | ,
       mode = 'BERT' then do
    needST = 1
    needJRI = 1
  end
  otherwise needST = 1
end

/* Initialize application stems. */
r_job. = ''
r_id. = ''
r_owner. = ''
r_queue. = ''
r_status. = ''
r_phase. = ''
r_sys. = ''
r_tg. = 0
r_pct. = 0
r_joe. = 0
r_bert. = 0
r_joebert. = 0
r_tok. = ''

jcnt. = 0
jtg. = 0
jpct. = 0
jjoe. = 0
jbert. = 0
jjoebert. = 0
jlabel. = ''
jkeys.0 = 0

qcnt. = 0
qlabel. = ''
qkeys.0 = 0

fcnt. = 0
ftg. = 0
fpct. = 0
fjoe. = 0
fbert. = 0
fjoebert. = 0
fdist. = 0
flabel. = ''
fkeys.0 = 0
fseen. = 0

hccnt. = 0
hcrec. = 0
hcpage. = 0
hclabel. = ''
hckeys.0 = 0

hjcnt. = 0
hjrec. = 0
hjlabel. = ''
hjkeys.0 = 0

hrcnt. = 0
hrlabel. = ''
hrkeys.0 = 0

h_job. = ''
h_class. = ''
h_rec. = 0
h_page. = 0
h_reason. = ''
h_rtext. = ''
h_dest. = ''
h_writer. = ''

occnt. = 0
ocrec. = 0
ocpage. = 0
oclabel. = ''
ockeys.0 = 0

o_job. = ''
o_class. = ''
o_rec. = 0
o_page. = 0
o_dest. = ''
o_writer. = ''
o_status. = ''

haspState. = 'UNKNOWN'
haspMsg. = ''
haspActive. = 0
haspAvail = 0
haspNote = ''
stAvail = 0
hAvail = 0
oAvail = 0
hasJoe = 0
hasBert = 0
hasJoeBert = 0
hasHBert = 0
stRows = 0
hRows = 0
oRows = 0
sysAvail = 0
sysRows = 0
jesSpoolPct = 0
jesSpoolSys = ''
jesSpoolName = ''
jesSpoolType = ''
visibleTGTotal = 0
visibleTGPct = 0

jriAvail = 0
jriRows = 0
jriJqeAvail = 0
jriJoeAvail = 0
jriBertAvail = 0
jriJqeUse = 0
jriJqePct = 0
jriJoeUse = 0
jriJoePct = 0
jriBertUse = 0
jriBertPct = 0

jriResAvail. = 0
jriUse. = 0
jriMax. = 0
jriPct. = 0
jriWarn. = 0
jriShort. = 'NO'
jriExh. = 'NO'
jriDesc. = ''
jriRich = 0

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

/* IBM examples use these values to see all authorized jobs. */
isfprefix = '**'
isfowner = '*'
isfsysname = '*'

/* Best-effort detection. Failure must never stop queue analysis. */
call DetectHasp050

if needSYS then call FetchSYS
if needJRI then call FetchJRI
if needST then call FetchST
if needH then call FetchH
if needO then call FetchO

call Header

select
  when mode = 'ALL' then do
    call Findings
    call ResolutionGuide 'AUTO'
    call ResourceSummary
    call ReportSpool
    call ReportJQE
    call ReportJOE
    call ReportBERT
    call ReportHeld
    call ReportOutput
    call ReportClasses
  end
  when mode = 'SPOOL' then do
    call ReportSpool
    call ResolutionGuide 'TG'
  end
  when mode = 'JQE' then do
    call ReportJQE
    call ResolutionGuide 'JQE'
  end
  when mode = 'JNUM' then do
    call ReportJQE
    call ResolutionGuide 'JNUM'
  end
  when mode = 'JOE' then do
    call ReportJOE
    call ResolutionGuide 'JOE'
  end
  when mode = 'BERT' then do
    call ReportBERT
    call ResolutionGuide 'BERT'
  end
  when mode = 'HELD' then call ReportHeld parm
  when mode = 'OUTPUT' then call ReportOutput parm
  when mode = 'CLASS' then call ReportOneClass parm
  when mode = 'JOB' then call ReportOneJob parm
  when mode = 'DETAIL' then call ReportDetail parm
  otherwise nop
end

call Footer
call Cleanup
call PresentReport
exit 0


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

FetchSYS:
/* Obtain the actual primary-JES spool utilization shown on SDSF SYS. */
drop SYSNAME. SPOOLPCT. JESNAME. JESTYPE.

isfcols = 'SYSNAME SPOOLPCT JESNAME JESTYPE'
Address SDSF "ISFEXEC SYS"
sysRc = rc

if sysRc <> 0 then do
  sysAvail = 0
  return
end

sysRows = isfrows
if sysRows = 0 then do
  sysAvail = 0
  return
end

localSys = translate(MVSVAR('SYSNAME'))
pick = 0

do si = 1 to sysRows
  if translate(strip(SYSNAME.si)) = localSys then do
    pick = si
    leave
  end
end

if pick = 0 then pick = 1

sysAvail = 1
jesSpoolPct = Num(SPOOLPCT.pick)
jesSpoolSys = strip(SYSNAME.pick)
jesSpoolName = strip(JESNAME.pick)
jesSpoolType = strip(JESTYPE.pick)
return


FetchJRI:
/* Obtain MAS-wide JES2 resource usage from the SDSF JRI panel.       */
drop NAME. NPRIVUSE. NPRIVPCT. NPRIVMAX. RESSHORT.
drop NPRIVSHORT. NPRIVEXH. WARNPCT. RESDESC.

isfcols = 'NAME NPRIVUSE NPRIVPCT NPRIVMAX RESSHORT NPRIVSHORT' ,
          'NPRIVEXH WARNPCT RESDESC'
Address SDSF "ISFEXEC JRI"
jriRc = rc

if jriRc = 0 then jriRich = 1
else do
  /* Fallback for SDSF levels without the newer warning columns. */
  drop NAME. NPRIVUSE. NPRIVPCT. NPRIVMAX. RESSHORT. RESDESC.
  isfcols = 'NAME NPRIVUSE NPRIVPCT NPRIVMAX RESSHORT RESDESC'
  Address SDSF "ISFEXEC JRI"
  jriRc = rc
  jriRich = 0
end

if jriRc <> 0 then do
  jriAvail = 0
  return
end

jriAvail = 1
jriRows = isfrows

do ji = 1 to jriRows
  rn = translate(strip(NAME.ji))
  jr = ''

  if left(rn,4) = 'JNUM' then jr = 'JNUM'
  else if left(rn,3) = 'JQE' then jr = 'JQE'
  else if left(rn,3) = 'JOE' then jr = 'JOE'
  else if left(rn,4) = 'BERT' then jr = 'BERT'
  else if left(rn,2) = 'TG' then jr = 'TG'

  if jr <> '' then do
    jriResAvail.jr = 1
    jriUse.jr = Num(NPRIVUSE.ji)
    jriMax.jr = Num(NPRIVMAX.ji)
    jriPct.jr = Num(NPRIVPCT.ji)
    jriShort.jr = translate(strip(RESSHORT.ji))
    jriDesc.jr = strip(RESDESC.ji)

    if jriRich then do
      jriWarn.jr = Num(WARNPCT.ji)
      if translate(strip(NPRIVSHORT.ji)) = 'YES' then
        jriShort.jr = 'YES'
      jriExh.jr = translate(strip(NPRIVEXH.ji))
    end
  end

  if left(rn,3) = 'JQE' then do
    jriJqeAvail = 1
    jriJqeUse = Num(NPRIVUSE.ji)
    jriJqePct = Num(NPRIVPCT.ji)
  end
  else if left(rn,3) = 'JOE' then do
    jriJoeAvail = 1
    jriJoeUse = Num(NPRIVUSE.ji)
    jriJoePct = Num(NPRIVPCT.ji)
  end
  else if left(rn,4) = 'BERT' then do
    jriBertAvail = 1
    jriBertUse = Num(NPRIVUSE.ji)
    jriBertPct = Num(NPRIVPCT.ji)
  end
end
return


FetchST:
/* Collect MAS-wide job/resource data visible to this SDSF user.       */
drop JNAME. JOBID. OWNERID. QUEUE. STATUS. PHASENAME. SYSNAME.
drop TGNUM. TGPCT. JOENUM. BERTNUM. JOEBERTNUM. TOKEN.

isfcols = 'JNAME JOBID OWNERID QUEUE STATUS PHASENAME SYSNAME' ,
          'TGNUM TGPCT JOENUM BERTNUM JOEBERTNUM'
Address SDSF "ISFEXEC ST"
stRc = rc

if stRc = 0 then do
  hasJoe = 1
  hasBert = 1
  hasJoeBert = 1
end
else do
  /* Retry without JOEBERTNUM for older SDSF/JES2 levels. */
  drop JNAME. JOBID. OWNERID. QUEUE. STATUS. PHASENAME. SYSNAME.
  drop TGNUM. TGPCT. JOENUM. BERTNUM. TOKEN.
  isfcols = 'JNAME JOBID OWNERID QUEUE STATUS PHASENAME SYSNAME' ,
            'TGNUM TGPCT JOENUM BERTNUM'
  Address SDSF "ISFEXEC ST"
  stRc = rc
  hasJoeBert = 0

  if stRc = 0 then do
    hasJoe = 1
    hasBert = 1
  end
  else do
    /* Final fallback keeps core spool analysis available. */
    drop JNAME. JOBID. OWNERID. QUEUE. STATUS. PHASENAME. SYSNAME.
    drop TGNUM. TGPCT. TOKEN.
    isfcols = 'JNAME JOBID OWNERID QUEUE STATUS PHASENAME SYSNAME' ,
              'TGNUM TGPCT'
    Address SDSF "ISFEXEC ST"
    stRc = rc
    hasJoe = 0
    hasBert = 0
    hasJoeBert = 0
  end
end

if stRc <> 0 then do
  stAvail = 0
  call Emit 'JOBDIAG: SDSF ST access failed. RC='stRc
  call SdsfMessages
  return
end

stAvail = 1
stRows = isfrows

do i = 1 to stRows
  jn = strip(JNAME.i)
  jid = strip(JOBID.i)
  own = strip(OWNERID.i)
  qu = strip(QUEUE.i)
  js = strip(STATUS.i)
  ph = strip(PHASENAME.i)
  sy = strip(SYSNAME.i)

  if jn = '' then jn = '(BLANK)'
  if jid = '' then jid = '-'
  if own = '' then own = '-'
  if qu = '' then qu = '(BLANK)'
  if js = '' then js = '-'
  if ph = '' then ph = '-'
  if sy = '' then sy = '-'

  tg = Num(TGNUM.i)
  pct = Num(TGPCT.i)

  joe = 0
  bert = 0
  joebert = 0
  if hasJoe then joe = Num(JOENUM.i)
  if hasBert then bert = Num(BERTNUM.i)
  if hasJoeBert then joebert = Num(JOEBERTNUM.i)

  r_job.i = jn
  r_id.i = jid
  r_owner.i = own
  r_queue.i = qu
  r_status.i = js
  r_phase.i = ph
  r_sys.i = sy
  r_tg.i = tg
  r_pct.i = pct
  r_joe.i = joe
  r_bert.i = bert
  r_joebert.i = joebert
  r_tok.i = TOKEN.i

  visibleTGTotal = visibleTGTotal + tg
  visibleTGPct = visibleTGPct + pct

  jk = c2x(jn)
  if jcnt.jk = 0 then do
    jkeys.0 = jkeys.0 + 1
    jnkey = jkeys.0
    jkeys.jnkey = jk
    jlabel.jk = jn
  end
  jcnt.jk = jcnt.jk + 1
  jtg.jk = jtg.jk + tg
  jpct.jk = jpct.jk + pct
  jjoe.jk = jjoe.jk + joe
  jbert.jk = jbert.jk + bert
  jjoebert.jk = jjoebert.jk + joebert

  qk = c2x(qu)
  if qcnt.qk = 0 then do
    qkeys.0 = qkeys.0 + 1
    qnkey = qkeys.0
    qkeys.qnkey = qk
    qlabel.qk = qu
  end
  qcnt.qk = qcnt.qk + 1

  fam = FamilyOf(jn)
  if fam <> '' then do
    fk = c2x(fam)
    if fcnt.fk = 0 then do
      fkeys.0 = fkeys.0 + 1
      fnkey = fkeys.0
      fkeys.fnkey = fk
      flabel.fk = fam
    end
    fcnt.fk = fcnt.fk + 1
    ftg.fk = ftg.fk + tg
    fpct.fk = fpct.fk + pct
    fjoe.fk = fjoe.fk + joe
    fbert.fk = fbert.fk + bert
    fjoebert.fk = fjoebert.fk + joebert

    sk = c2x(fam'|'jn)
    if fseen.sk = 0 then do
      fseen.sk = 1
      fdist.fk = fdist.fk + 1
    end
  end
end
return

FetchH:
drop JNAME. JOBID. OWNERID. OCLASS. OUTDISP. DESTN.
drop RECCNT. PAGECNT. STATUS. WTRID. OHREASON. OHRSNTXT.
drop BERTNUM.

isfcols = 'JNAME JOBID OWNERID OCLASS OUTDISP DESTN RECCNT' ,
          'PAGECNT STATUS WTRID OHREASON OHRSNTXT BERTNUM'
Address SDSF "ISFEXEC H"
hRc = rc

if hRc = 0 then hasHBert = 1
else do
  drop JNAME. JOBID. OWNERID. OCLASS. OUTDISP. DESTN.
  drop RECCNT. PAGECNT. STATUS. WTRID. OHREASON. OHRSNTXT.
  isfcols = 'JNAME JOBID OWNERID OCLASS OUTDISP DESTN RECCNT' ,
            'PAGECNT STATUS WTRID OHREASON OHRSNTXT'
  Address SDSF "ISFEXEC H"
  hRc = rc
  hasHBert = 0
end

if hRc <> 0 then do
  hAvail = 0
  call Emit 'JOBDIAG: SDSF H access failed. RC='hRc
  call SdsfMessages
  return
end

hAvail = 1
hRows = isfrows

do i = 1 to hRows
  jn = strip(JNAME.i)
  oc = strip(OCLASS.i)
  rr = strip(OHREASON.i)
  rt = strip(OHRSNTXT.i)
  ds = strip(DESTN.i)
  wr = strip(WTRID.i)

  if jn = '' then jn = '(BLANK)'
  if oc = '' then oc = '(BLANK)'
  if rr = '' then rr = '-'
  if rt = '' then rt = 'No reason text'
  if ds = '' then ds = '(BLANK)'
  if wr = '' then wr = '(NONE)'

  recs = Num(RECCNT.i)
  pages = Num(PAGECNT.i)

  h_job.i = jn
  h_class.i = oc
  h_rec.i = recs
  h_page.i = pages
  h_reason.i = rr
  h_rtext.i = rt
  h_dest.i = ds
  h_writer.i = wr

  ck = c2x(oc)
  if hccnt.ck = 0 then do
    hckeys.0 = hckeys.0 + 1
    hcnkey = hckeys.0
    hckeys.hcnkey = ck
    hclabel.ck = oc
  end
  hccnt.ck = hccnt.ck + 1
  hcrec.ck = hcrec.ck + recs
  hcpage.ck = hcpage.ck + pages

  jk = c2x(jn)
  if hjcnt.jk = 0 then do
    hjkeys.0 = hjkeys.0 + 1
    hjnkey = hjkeys.0
    hjkeys.hjnkey = jk
    hjlabel.jk = jn
  end
  hjcnt.jk = hjcnt.jk + 1
  hjrec.jk = hjrec.jk + recs

  rkey = c2x(rr'|'rt)
  if hrcnt.rkey = 0 then do
    hrkeys.0 = hrkeys.0 + 1
    hrnkey = hrkeys.0
    hrkeys.hrnkey = rkey
    hrlabel.rkey = rr' 'rt
  end
  hrcnt.rkey = hrcnt.rkey + 1
end
return

FetchO:
drop JNAME. JOBID. OWNERID. OCLASS. DESTN. RECCNT.
drop PAGECNT. STATUS. WTRID. OUTDISP.

isfcols = 'JNAME JOBID OWNERID OCLASS DESTN RECCNT PAGECNT' ,
          'STATUS WTRID OUTDISP'
Address SDSF "ISFEXEC O"
oRc = rc

if oRc <> 0 then do
  oAvail = 0
  call Emit 'JOBDIAG: SDSF O access failed. RC='oRc
  call SdsfMessages
  return
end

oAvail = 1
oRows = isfrows

do i = 1 to oRows
  jn = strip(JNAME.i)
  oc = strip(OCLASS.i)
  ds = strip(DESTN.i)
  wr = strip(WTRID.i)
  st = strip(STATUS.i)

  if jn = '' then jn = '(BLANK)'
  if oc = '' then oc = '(BLANK)'
  if ds = '' then ds = '(BLANK)'
  if wr = '' then wr = '(NONE)'
  if st = '' then st = '-'

  recs = Num(RECCNT.i)
  pages = Num(PAGECNT.i)

  o_job.i = jn
  o_class.i = oc
  o_rec.i = recs
  o_page.i = pages
  o_dest.i = ds
  o_writer.i = wr
  o_status.i = st

  ck = c2x(oc)
  if occnt.ck = 0 then do
    ockeys.0 = ockeys.0 + 1
    ocnkey = ockeys.0
    ockeys.ocnkey = ck
    oclabel.ck = oc
  end
  occnt.ck = occnt.ck + 1
  ocrec.ck = ocrec.ck + recs
  ocpage.ck = ocpage.ck + pages
end
return

/*====================================================================*/
/* $HASP050 DETECTION                                                 */
/*====================================================================*/

DetectHasp050:
/* Current-day SYSLOG is used intentionally for broad compatibility. */
/* A continuing HASP050 is periodically reissued until relief.       */
drop isfstartlinetoken
drop isfnextlinetoken
drop isfline.

isffind = '$HASP05'
isffindlim = 9999999
isfscrolltype = 'FINDNEXT'
isflinelim = 1

logLoops = 0
do forever
  Address SDSF "ISFLOG READ TYPE(SYSLOG)"
  logRc = rc

  if logRc <> 0 then do
    haspAvail = 0
    haspNote = 'SYSLOG unavailable or not authorized'
    leave
  end

  haspAvail = 1
  logLoops = logLoops + 1
  if logLoops > 5000 then do
    haspNote = 'SYSLOG search stopped at 5000 HASP05 matches'
    leave
  end

  if isfline.0 > 0 then do
    ln = translate(isfline.1)
    if pos('$HASP050', ln) > 0 then do
      res = HaspResource(ln)
      if res <> '' then do
        haspState.res = 'ACTIVE'
        haspMsg.res = strip(isfline.1)
      end
    end
    if pos('$HASP055', ln) > 0 then do
      res = HaspResource(ln)
      if res <> '' then do
        haspState.res = 'RELIEVED'
        haspMsg.res = strip(isfline.1)
      end
    end
  end

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

drop isffind isffindlim isfscrolltype isflinelim
drop isfstartlinetoken isfnextlinetoken

do z = 1 to words('TG JQE JNUM JOE BERT')
  res = word('TG JQE JNUM JOE BERT', z)
  if haspState.res = 'ACTIVE' then haspActive.res = 1
end
return


HaspResource:
procedure
parse upper arg text
if pos('JQES', text) > 0 then return 'JQE'
if pos('JOES', text) > 0 then return 'JOE'
if pos('JNUM', text) > 0 then return 'JNUM'
if pos('TGS', text) > 0 then return 'TG'
if pos('BERT', text) > 0 then return 'BERT'
return ''


/*====================================================================*/
/* REPORTS                                                            */
/*====================================================================*/

Header:
call Emit ''
call Emit copies('=', 78)
call Emit ' JOBDIAG - JES2 RESOURCE TRIAGE'
call Emit ' Author: SARATH S'
call Emit ' System:' mvsvar('SYSNAME')
call Emit ' Date:' DisplayDate() ' Time:' time('N') ' Mode:' mode parm
call Emit copies('=', 78)

if haspAvail then do
  act = ''
  do z = 1 to words('TG JQE JNUM JOE BERT')
    res = word('TG JQE JNUM JOE BERT', z)
    if haspState.res = 'ACTIVE' then act = act res
  end
  act = strip(act)
  if act <> '' then do
    call Emit ' *** ACTIVE/RECENT $HASP050 STATE DETECTED:' act '***'
    call Emit ' Sections for those resources are marked PRIORITY.'
  end
  else call Emit ' $HASP050 scan: no current-day active state identified.'
end
else call Emit ' $HASP050 scan:' haspNote
return


ResourceSummary:
call Section 'RESOURCE / DATA SUMMARY'

if sysAvail then do
  call Emit ' JES SPOOL%            :' FormatPct(jesSpoolPct)
  call Emit ' JES/system            :' jesSpoolName jesSpoolSys jesSpoolType
end
else call Emit ' JES SPOOL%            : unavailable from SDSF SYS'

if jriJqeAvail then
  call Emit ' JQEs                  :' CountPct(jriJqeUse,jriJqePct)
else
  call Emit ' JQEs                  : unavailable from SDSF JRI'

if jriJoeAvail then
  call Emit ' JOEs                  :' CountPct(jriJoeUse,jriJoePct)
else
  call Emit ' JOEs                  : unavailable from SDSF JRI'

if jriBertAvail then
  call Emit ' BERTs                 :' CountPct(jriBertUse,jriBertPct)
else
  call Emit ' BERTs                 : unavailable from SDSF JRI'

if hAvail then call Emit ' Held output groups    :' hRows
else call Emit ' H data                : unavailable'

if oAvail then call Emit ' Non-held output groups:' oRows
else call Emit ' O data                : unavailable'

call Emit ' SDSF SYSNAME filter   : * (all authorized systems)'
return

ReportSpool:
call PrioritySection 'SPOOL / TRACK GROUP ANALYSIS', 'TG'

if \stAvail then do
  call Emit 'ST data unavailable; spool consumer analysis cannot be produced.'
  return
end

call Emit 'CURRENT SPOOL STATE'
if sysAvail then do
  call Emit 'Current JES SPOOL%        :' FormatPct(jesSpoolPct)
  call Emit 'Visible ST job TG%        :' FormatPct(visibleTGPct)
  otherPct = jesSpoolPct - visibleTGPct
  if otherPct < 0 then otherPct = 0
  call Emit 'Other/unseen TG%          :' FormatPct(otherPct)
end
else call Emit 'Current JES SPOOL%        : unavailable from SDSF SYS'
call Emit 'Visible ST job TGs        :' FormatInt(visibleTGTotal)
call Emit ''
call Emit 'The actual Spool% is not the sum of visible ST TGPct rows.'
call Emit 'The gap can include JES-managed/internal spool plus jobs hidden'
call Emit 'by SDSF/SAF visibility or filters, with minor rounding effects.'
call Emit ''

call Emit 'TOP INDIVIDUAL JOBS BY TG PERCENT'
call Emit 'Jobname  JobID     Owner    Queue       TGs       TG%'
call Emit copies('-', 67)

call TopReset
do i = 1 to stRows
  call TopAdd i, r_pct.i
end
do x = 1 to topCount
  i = topKey.x
  call Emit left(r_job.i,8) left(r_id.i,9) left(r_owner.i,8) ,
      left(r_queue.i,10) right(FormatInt(r_tg.i),9) ,
      right(FormatPct(r_pct.i),7)
end

call Emit ''
call Emit 'TOP NON-EXECUTING JOBS BY TG PERCENT'
call Emit 'Jobname  JobID     Owner    Queue       TGs       TG%'
call Emit copies('-', 67)
call TopReset
do i = 1 to stRows
  if translate(r_phase.i) <> 'EXECUTING' then call TopAdd i, r_pct.i
end
do x = 1 to topCount
  i = topKey.x
  call Emit left(r_job.i,8) left(r_id.i,9) left(r_owner.i,8) ,
      left(r_queue.i,10) right(FormatInt(r_tg.i),9) ,
      right(FormatPct(r_pct.i),7)
end
call Emit 'This view suppresses active EXECUTING rows only.'

call Emit ''
call Emit 'TOP EXACT JOBNAME AGGREGATES BY TG PERCENT'
call Emit 'Jobname  Jobs       TGs        TG%  Flag'
call Emit copies('-', 55)
call TopReset
do x = 1 to jkeys.0
  k = jkeys.x
  call TopAdd k, jpct.k
end
do x = 1 to topCount
  k = topKey.x
  flag = ''
  if jpct.k >= groupPctWarn then flag = '<-- SIGNIFICANT'
  call Emit left(jlabel.k,8) right(jcnt.k,6) ,
      right(FormatInt(jtg.k),10) right(FormatPct(jpct.k),9) flag
end

call Emit ''
call Emit 'NUMERIC-SUFFIX JOB FAMILIES'
call Emit 'Family    Names  Jobs       TGs        TG%  Flag'
call Emit copies('-', 62)
call TopReset
do x = 1 to fkeys.0
  k = fkeys.x
  if fdist.k >= 2 then call TopAdd k, fpct.k
end
if topCount = 0 then call Emit 'No multi-name numeric-suffix families found.'
do x = 1 to topCount
  k = topKey.x
  flag = ''
  if fpct.k >= familyPctWarn then flag = '<-- SIGNIFICANT'
  call Emit left(flabel.k,8) right(fdist.k,5) right(fcnt.k,6) ,
      right(FormatInt(ftg.k),10) right(FormatPct(fpct.k),9) flag
end

call Emit ''
call Emit 'Family rule: only job names ending in two digits are grouped.'
call Emit 'Example: ABCDEF20 and ABCDEF30 are reported as ABCDEF##.'
return


ReportJQE:
resMark = 'JQE'
if mode = 'JNUM' then resMark = 'JNUM'
call PrioritySection 'JOB QUEUE / JQE / JNUM ANALYSIS', resMark

if jriJqeAvail then
  call Emit 'Current JQEs :' CountPct(jriJqeUse,jriJqePct)
else
  call Emit 'Current JQEs : unavailable from SDSF JRI'
call Emit ''

if \stAvail then do
  call Emit 'ST data unavailable; queue analysis cannot be produced.'
  return
end

call Emit 'Visible ST rows:' stRows
call Emit 'This is a diagnostic job-count view, not checkpoint inventory.'
call Emit ''

call Emit 'JOBS BY JES QUEUE'
call Emit 'Queue       Jobs      Share'
call Emit copies('-', 32)
call TopReset
do x = 1 to qkeys.0
  k = qkeys.x
  call TopAdd k, qcnt.k
end
do x = 1 to topCount
  k = topKey.x
  share = 0
  if stRows > 0 then share = qcnt.k * 100 / stRows
  call Emit left(qlabel.k,10) right(qcnt.k,9) right(FormatPct(share),9)
end

call Emit ''
call Emit 'MOST REPEATED EXACT JOBNAMES'
call Emit 'Jobname    Instances   TG%'
call Emit copies('-', 34)
call TopReset
do x = 1 to jkeys.0
  k = jkeys.x
  call TopAdd k, jcnt.k
end
do x = 1 to topCount
  k = topKey.x
  flag = ''
  if jcnt.k >= repeatWarn then flag = '<-- HIGH COUNT'
  call Emit left(jlabel.k,8) right(jcnt.k,10) ,
      right(FormatPct(jpct.k),8) flag
end

call Emit ''
call Emit 'MOST REPEATED NUMERIC-SUFFIX FAMILIES'
call Emit 'Family    Names  Instances'
call Emit copies('-', 34)
call TopReset
do x = 1 to fkeys.0
  k = fkeys.x
  if fdist.k >= 2 then call TopAdd k, fcnt.k
end
if topCount = 0 then call Emit 'No multi-name numeric-suffix families found.'
do x = 1 to topCount
  k = topKey.x
  call Emit left(flabel.k,8) right(fdist.k,5) right(fcnt.k,10)
end
return

ReportJOE:
call PrioritySection 'JOE ANALYSIS', 'JOE'

if jriJoeAvail then
  call Emit 'Current JOEs :' CountPct(jriJoeUse,jriJoePct)
else
  call Emit 'Current JOEs : unavailable from SDSF JRI'
call Emit ''

if \stAvail then do
  call Emit 'ST data unavailable; JOE analysis cannot be produced.'
  return
end

if \hasJoe then do
  call Emit 'ST JOENUM is unavailable on this SDSF/JES level.'
  if hAvail | oAvail then do
    call Emit 'Fallback output-group counts are available in H/O sections.'
  end
  else call Emit 'Run JOBDIAG ALL to attempt H/O output-group analysis.'
  return
end

call Emit 'TOP INDIVIDUAL JOBS BY JOE COUNT'
call Emit 'Jobname  JobID       JOEs  Status'
call Emit copies('-', 55)
call TopReset
do i = 1 to stRows
  call TopAdd i, r_joe.i
end
do x = 1 to topCount
  i = topKey.x
  call Emit left(r_job.i,8) left(r_id.i,10) ,
      right(FormatInt(r_joe.i),8) left(r_status.i,17)
end

call Emit ''
call Emit 'TOP EXACT JOBNAME AGGREGATES BY JOEs'
call Emit 'Jobname  Jobs       JOEs'
call Emit copies('-', 34)
call TopReset
do x = 1 to jkeys.0
  k = jkeys.x
  call TopAdd k, jjoe.k
end
do x = 1 to topCount
  k = topKey.x
  call Emit left(jlabel.k,8) right(jcnt.k,6) ,
      right(FormatInt(jjoe.k),10)
end

call Emit ''
call Emit 'TOP NUMERIC-SUFFIX FAMILIES BY JOEs'
call Emit 'Family    Names  Jobs       JOEs'
call Emit copies('-', 41)
call TopReset
do x = 1 to fkeys.0
  k = fkeys.x
  if fdist.k >= 2 then call TopAdd k, fjoe.k
end
if topCount = 0 then call Emit 'No multi-name numeric-suffix families found.'
do x = 1 to topCount
  k = topKey.x
  call Emit left(flabel.k,8) right(fdist.k,5) right(fcnt.k,6) ,
      right(FormatInt(fjoe.k),10)
end
return


ReportBERT:
call PrioritySection 'BERT ANALYSIS', 'BERT'

if jriBertAvail then
  call Emit 'Current BERTs :' CountPct(jriBertUse,jriBertPct)
else
  call Emit 'Current BERTs : unavailable from SDSF JRI'
call Emit ''

if \stAvail then do
  call Emit 'ST data unavailable; BERT analysis cannot be produced.'
  return
end

if \hasBert then do
  call Emit 'TOP INDIVIDUAL JOBS BY BERT FOOTPRINT'
  if hasJoeBert then
    call Emit 'Jobname  JobID      JobBERT  JOEBERT    Total'
  else
    call Emit 'Jobname  JobID        BERTs'
  call Emit copies('-', 58)
  call TopReset
  do i = 1 to stRows
    btotal = r_bert.i + r_joebert.i
    call TopAdd i, btotal
  end
  do x = 1 to topCount
    i = topKey.x
    if hasJoeBert then do
      btotal = r_bert.i + r_joebert.i
      call Emit left(r_job.i,8) left(r_id.i,10) ,
          right(FormatInt(r_bert.i),8) right(FormatInt(r_joebert.i),8) ,
          right(FormatInt(btotal),8)
    end
    else
      call Emit left(r_job.i,8) left(r_id.i,10) ,
          right(FormatInt(r_bert.i),10)
  end

  call Emit ''
  call Emit 'TOP EXACT JOBNAME AGGREGATES BY BERT FOOTPRINT'
  if hasJoeBert then
    call Emit 'Jobname  Jobs     JobBERT   JOEBERT     Total'
  else
    call Emit 'Jobname  Jobs      BERTs'
  call Emit copies('-', 58)
  call TopReset
  do x = 1 to jkeys.0
    k = jkeys.x
    btotal = jbert.k + jjoebert.k
    call TopAdd k, btotal
  end
  do x = 1 to topCount
    k = topKey.x
    if hasJoeBert then do
      btotal = jbert.k + jjoebert.k
      call Emit left(jlabel.k,8) right(jcnt.k,6) ,
          right(FormatInt(jbert.k),10) right(FormatInt(jjoebert.k),9) ,
          right(FormatInt(btotal),9)
    end
    else
      call Emit left(jlabel.k,8) right(jcnt.k,6) ,
          right(FormatInt(jbert.k),10)
  end

  if hasJoeBert then do
    call Emit ''
    call Emit 'JOEBERT is BERT usage for the job''s JOEs.'
    call Emit 'Total above is JobBERT + JOEBERT for triage/ranking.'
  end
end
else call Emit 'ST BERTNUM is unavailable on this SDSF/JES level.'

if hAvail & hasHBert then do
  call Emit ''
  call Emit 'H also exposes BERTs per held JOE; ST JOEBERT is preferred'
  call Emit 'for job-level ranking so the same JOEs are not counted twice.'
end
return

ReportHeld:
parse upper arg want
want = strip(want)

if want <> '' then do
  call Section 'HELD OUTPUT CLASS ANALYSIS: 'want
  if \hAvail then do
    call Emit 'H data unavailable; held-output analysis cannot be produced.'
    return
  end

  groups = 0
  recs = 0
  pages = 0
  tjcnt. = 0
  tjrec. = 0
  tjlabel. = ''
  tjkeys.0 = 0
  trcnt. = 0
  trlabel. = ''
  trkeys.0 = 0
  tdcnt. = 0
  tdlabel. = ''
  tdkeys.0 = 0
  twcnt. = 0
  twlabel. = ''
  twkeys.0 = 0

  do i = 1 to hRows
    if translate(h_class.i) <> want then iterate
    groups = groups + 1
    recs = recs + h_rec.i
    pages = pages + h_page.i

    jk = c2x(h_job.i)
    if tjcnt.jk = 0 then do
      tjkeys.0 = tjkeys.0 + 1
      nx = tjkeys.0
      tjkeys.nx = jk
      tjlabel.jk = h_job.i
    end
    tjcnt.jk = tjcnt.jk + 1
    tjrec.jk = tjrec.jk + h_rec.i

    rk = c2x(h_reason.i'|'h_rtext.i)
    if trcnt.rk = 0 then do
      trkeys.0 = trkeys.0 + 1
      nx = trkeys.0
      trkeys.nx = rk
      trlabel.rk = h_reason.i' 'h_rtext.i
    end
    trcnt.rk = trcnt.rk + 1

    dk = c2x(h_dest.i)
    if tdcnt.dk = 0 then do
      tdkeys.0 = tdkeys.0 + 1
      nx = tdkeys.0
      tdkeys.nx = dk
      tdlabel.dk = h_dest.i
    end
    tdcnt.dk = tdcnt.dk + 1

    if h_writer.i <> '(NONE)' then do
      wk = c2x(h_writer.i)
      if twcnt.wk = 0 then do
        twkeys.0 = twkeys.0 + 1
        nx = twkeys.0
        twkeys.nx = wk
        twlabel.wk = h_writer.i
      end
      twcnt.wk = twcnt.wk + 1
    end
  end

  share = 0
  if hRows > 0 then share = groups * 100 / hRows
  call Emit 'Held groups :' groups '  Share of held:' FormatPct(share)
  call Emit 'Records     :' FormatInt(recs)
  call Emit 'Pages       :' FormatInt(pages)

  if groups = 0 then return

  call Emit ''
  call Emit 'TOP JOBNAMES IN HELD CLASS 'want
  call Emit 'Jobname      Groups       Records'
  call Emit copies('-', 39)
  call TopReset
  do x = 1 to tjkeys.0
    k = tjkeys.x
    call TopAdd k, tjcnt.k
  end
  do x = 1 to topCount
    k = topKey.x
    call Emit left(tjlabel.k,8) right(tjcnt.k,10) ,
        right(FormatInt(tjrec.k),13)
  end

  call Emit ''
  call Emit 'TOP HOLD REASONS IN CLASS 'want
  call Emit 'Groups  Reason'
  call Emit copies('-', 70)
  call TopReset
  do x = 1 to trkeys.0
    k = trkeys.x
    call TopAdd k, trcnt.k
  end
  do x = 1 to topCount
    k = topKey.x
    call Emit right(trcnt.k,6) left(trlabel.k,68)
  end

  call Emit ''
  call Emit 'TOP DESTINATIONS IN HELD CLASS 'want
  call Emit 'Destination          Groups'
  call Emit copies('-', 31)
  call TopReset
  do x = 1 to tdkeys.0
    k = tdkeys.x
    call TopAdd k, tdcnt.k
  end
  do x = 1 to topCount
    k = topKey.x
    call Emit left(tdlabel.k,18) right(tdcnt.k,10)
  end

  if twkeys.0 > 0 then do
    call Emit ''
    call Emit 'TOP EXTERNAL WRITERS IN HELD CLASS 'want
    call Emit 'Writer       Groups'
    call Emit copies('-', 24)
    call TopReset
    do x = 1 to twkeys.0
      k = twkeys.x
      call TopAdd k, twcnt.k
    end
    do x = 1 to topCount
      k = topKey.x
      call Emit left(twlabel.k,8) right(twcnt.k,10)
    end
  end
  return
end

call PrioritySection 'HELD OUTPUT ANALYSIS', 'JOE'

if \hAvail then do
  call Emit 'H data unavailable; held-output analysis cannot be produced.'
  return
end

call Emit 'Held output groups visible:' hRows
call Emit ''

call Emit 'TOP HELD OUTPUT CLASSES BY OUTPUT-GROUP COUNT'
call Emit 'Class       Groups      Share       Records  Flag'
call Emit copies('-', 62)
call TopReset
do x = 1 to hckeys.0
  k = hckeys.x
  call TopAdd k, hccnt.k
end
do x = 1 to topCount
  k = topKey.x
  share = 0
  if hRows > 0 then share = hccnt.k * 100 / hRows
  flag = ''
  if share >= classShareWarn then flag = '<-- CONCENTRATION'
  call Emit left(hclabel.k,8) right(hccnt.k,10) ,
      right(FormatPct(share),10) right(FormatInt(hcrec.k),13) flag
end

call Emit ''
call Emit 'TOP JOBNAMES IN HELD OUTPUT'
call Emit 'Jobname     Groups       Records'
call Emit copies('-', 39)
call TopReset
do x = 1 to hjkeys.0
  k = hjkeys.x
  call TopAdd k, hjcnt.k
end
do x = 1 to topCount
  k = topKey.x
  call Emit left(hjlabel.k,8) right(hjcnt.k,10) ,
      right(FormatInt(hjrec.k),13)
end

call Emit ''
call Emit 'TOP HOLD REASONS'
call Emit 'Groups  Reason'
call Emit copies('-', 70)
call TopReset
do x = 1 to hrkeys.0
  k = hrkeys.x
  call TopAdd k, hrcnt.k
end
do x = 1 to topCount
  k = topKey.x
  call Emit right(hrcnt.k,6) left(hrlabel.k,68)
end
return


ReportOutput:
parse upper arg want
want = strip(want)

if want <> '' then do
  call Section 'NON-HELD OUTPUT CLASS ANALYSIS: 'want
  if \oAvail then do
    call Emit 'O data unavailable; output analysis cannot be produced.'
    return
  end

  groups = 0
  recs = 0
  pages = 0
  tjcnt. = 0
  tjrec. = 0
  tjlabel. = ''
  tjkeys.0 = 0
  tdcnt. = 0
  tdlabel. = ''
  tdkeys.0 = 0
  twcnt. = 0
  twlabel. = ''
  twkeys.0 = 0

  do i = 1 to oRows
    if translate(o_class.i) <> want then iterate
    groups = groups + 1
    recs = recs + o_rec.i
    pages = pages + o_page.i

    jk = c2x(o_job.i)
    if tjcnt.jk = 0 then do
      tjkeys.0 = tjkeys.0 + 1
      nx = tjkeys.0
      tjkeys.nx = jk
      tjlabel.jk = o_job.i
    end
    tjcnt.jk = tjcnt.jk + 1
    tjrec.jk = tjrec.jk + o_rec.i

    dk = c2x(o_dest.i)
    if tdcnt.dk = 0 then do
      tdkeys.0 = tdkeys.0 + 1
      nx = tdkeys.0
      tdkeys.nx = dk
      tdlabel.dk = o_dest.i
    end
    tdcnt.dk = tdcnt.dk + 1

    if o_writer.i <> '(NONE)' then do
      wk = c2x(o_writer.i)
      if twcnt.wk = 0 then do
        twkeys.0 = twkeys.0 + 1
        nx = twkeys.0
        twkeys.nx = wk
        twlabel.wk = o_writer.i
      end
      twcnt.wk = twcnt.wk + 1
    end
  end

  share = 0
  if oRows > 0 then share = groups * 100 / oRows
  call Emit 'Output groups :' groups '  Share of non-held:' FormatPct(share)
  call Emit 'Records       :' FormatInt(recs)
  call Emit 'Pages         :' FormatInt(pages)

  if groups = 0 then return

  call Emit ''
  call Emit 'TOP JOBNAMES IN OUTPUT CLASS 'want
  call Emit 'Jobname      Groups       Records'
  call Emit copies('-', 39)
  call TopReset
  do x = 1 to tjkeys.0
    k = tjkeys.x
    call TopAdd k, tjcnt.k
  end
  do x = 1 to topCount
    k = topKey.x
    call Emit left(tjlabel.k,8) right(tjcnt.k,10) ,
        right(FormatInt(tjrec.k),13)
  end

  call Emit ''
  call Emit 'TOP DESTINATIONS IN OUTPUT CLASS 'want
  call Emit 'Destination          Groups'
  call Emit copies('-', 31)
  call TopReset
  do x = 1 to tdkeys.0
    k = tdkeys.x
    call TopAdd k, tdcnt.k
  end
  do x = 1 to topCount
    k = topKey.x
    call Emit left(tdlabel.k,18) right(tdcnt.k,10)
  end

  if twkeys.0 > 0 then do
    call Emit ''
    call Emit 'TOP EXTERNAL WRITERS IN OUTPUT CLASS 'want
    call Emit 'Writer       Groups'
    call Emit copies('-', 24)
    call TopReset
    do x = 1 to twkeys.0
      k = twkeys.x
      call TopAdd k, twcnt.k
    end
    do x = 1 to topCount
      k = topKey.x
      call Emit left(twlabel.k,8) right(twcnt.k,10)
    end
  end
  return
end

call Section 'NON-HELD OUTPUT QUEUE ANALYSIS'

if \oAvail then do
  call Emit 'O data unavailable; output analysis cannot be produced.'
  return
end

call Emit 'Non-held output groups visible:' oRows
call Emit ''

call Emit 'TOP NON-HELD OUTPUT CLASSES'
call Emit 'Class       Groups      Share       Records  Flag'
call Emit copies('-', 62)
call TopReset
do x = 1 to ockeys.0
  k = ockeys.x
  call TopAdd k, occnt.k
end
do x = 1 to topCount
  k = topKey.x
  share = 0
  if oRows > 0 then share = occnt.k * 100 / oRows
  flag = ''
  if share >= classShareWarn then flag = '<-- CONCENTRATION'
  call Emit left(oclabel.k,8) right(occnt.k,10) ,
      right(FormatPct(share),10) right(FormatInt(ocrec.k),13) flag
end

tjcnt. = 0
tjrec. = 0
tjlabel. = ''
tjkeys.0 = 0
tdcnt. = 0
tdlabel. = ''
tdkeys.0 = 0
twcnt. = 0
twlabel. = ''
twkeys.0 = 0

do i = 1 to oRows
  jk = c2x(o_job.i)
  if tjcnt.jk = 0 then do
    tjkeys.0 = tjkeys.0 + 1
    nx = tjkeys.0
    tjkeys.nx = jk
    tjlabel.jk = o_job.i
  end
  tjcnt.jk = tjcnt.jk + 1
  tjrec.jk = tjrec.jk + o_rec.i

  dk = c2x(o_dest.i)
  if tdcnt.dk = 0 then do
    tdkeys.0 = tdkeys.0 + 1
    nx = tdkeys.0
    tdkeys.nx = dk
    tdlabel.dk = o_dest.i
  end
  tdcnt.dk = tdcnt.dk + 1

  if o_writer.i <> '(NONE)' then do
    wk = c2x(o_writer.i)
    if twcnt.wk = 0 then do
      twkeys.0 = twkeys.0 + 1
      nx = twkeys.0
      twkeys.nx = wk
      twlabel.wk = o_writer.i
    end
    twcnt.wk = twcnt.wk + 1
  end
end

call Emit ''
call Emit 'TOP JOBNAMES IN NON-HELD OUTPUT'
call Emit 'Jobname      Groups       Records'
call Emit copies('-', 39)
call TopReset
do x = 1 to tjkeys.0
  k = tjkeys.x
  call TopAdd k, tjcnt.k
end
do x = 1 to topCount
  k = topKey.x
  call Emit left(tjlabel.k,8) right(tjcnt.k,10) ,
      right(FormatInt(tjrec.k),13)
end

call Emit ''
call Emit 'TOP DESTINATIONS IN NON-HELD OUTPUT'
call Emit 'Destination          Groups'
call Emit copies('-', 31)
call TopReset
do x = 1 to tdkeys.0
  k = tdkeys.x
  call TopAdd k, tdcnt.k
end
do x = 1 to topCount
  k = topKey.x
  call Emit left(tdlabel.k,18) right(tdcnt.k,10)
end

if twkeys.0 > 0 then do
  call Emit ''
  call Emit 'TOP EXTERNAL WRITERS IN NON-HELD OUTPUT'
  call Emit 'Writer       Groups'
  call Emit copies('-', 24)
  call TopReset
  do x = 1 to twkeys.0
    k = twkeys.x
    call TopAdd k, twcnt.k
  end
  do x = 1 to topCount
    k = topKey.x
    call Emit left(twlabel.k,8) right(twcnt.k,10)
  end
end
return


ReportClasses:
call Section 'COMBINED SYSOUT CLASS ANALYSIS'

if \hAvail & \oAvail then do
  call Emit 'Neither H nor O data is available.'
  return
end

call Emit 'Class      Held   NonHeld     Total    Share       Records  Flag'
call Emit copies('-', 76)

ccnt. = 0
crec. = 0
clabel. = ''
ckeys.0 = 0

if hAvail then do x = 1 to hckeys.0
  k = hckeys.x
  if ccnt.k = 0 then do
    ckeys.0 = ckeys.0 + 1
    nx = ckeys.0
    ckeys.nx = k
    clabel.k = hclabel.k
  end
  ccnt.k = ccnt.k + hccnt.k
  crec.k = crec.k + hcrec.k
end

if oAvail then do x = 1 to ockeys.0
  k = ockeys.x
  if ccnt.k = 0 then do
    ckeys.0 = ckeys.0 + 1
    nx = ckeys.0
    ckeys.nx = k
    clabel.k = oclabel.k
  end
  ccnt.k = ccnt.k + occnt.k
  crec.k = crec.k + ocrec.k
end

call TopReset
do x = 1 to ckeys.0
  k = ckeys.x
  call TopAdd k, ccnt.k
end

totalGroups = 0
if hAvail then totalGroups = totalGroups + hRows
if oAvail then totalGroups = totalGroups + oRows

do x = 1 to topCount
  k = topKey.x
  hv = hccnt.k
  ov = occnt.k
  share = 0
  if totalGroups > 0 then share = ccnt.k * 100 / totalGroups
  flag = ''
  if share >= classShareWarn then flag = '<-- CONCENTRATION'
  call Emit left(clabel.k,8) right(hv,7) right(ov,9) right(ccnt.k,9) ,
      right(FormatPct(share),9) right(FormatInt(crec.k),13) flag
end
return


ReportOneClass:
parse upper arg want
want = strip(want)
call Section 'COMBINED SYSOUT CLASS ANALYSIS: 'want

hk = c2x(want)
hv = hccnt.hk
ov = occnt.hk
hrec = hcrec.hk
orec = ocrec.hk
hp = hcpage.hk
op = ocpage.hk
totgrp = hv + ov
totrec = hrec + orec
totpage = hp + op

call Emit 'Held groups      :' hv
call Emit 'Non-held groups  :' ov
call Emit 'Total groups     :' totgrp
call Emit 'Held records     :' FormatInt(hrec)
call Emit 'Non-held records :' FormatInt(orec)
call Emit 'Total records    :' FormatInt(totrec)
call Emit 'Total pages      :' FormatInt(totpage)

call ReportHeld want
call ReportOutput want
return

ReportOneJob:
parse upper arg pat
pat = strip(pat)
call Section 'TARGETED JOB ANALYSIS: 'pat

if \stAvail then do
  call Emit 'ST data unavailable.'
  return
end

matches = 0
sumTG = 0
sumPct = 0
sumJoe = 0
sumBert = 0
sumJoeBert = 0

do i = 1 to stRows
  if WildMatch(r_job.i, pat) then do
    matches = matches + 1
    sumTG = sumTG + r_tg.i
    sumPct = sumPct + r_pct.i
    sumJoe = sumJoe + r_joe.i
    sumBert = sumBert + r_bert.i
    sumJoeBert = sumJoeBert + r_joebert.i
  end
end

call Emit 'Matching jobs :' matches
call Emit 'Total TGs     :' FormatInt(sumTG)
call Emit 'Total TG pct  :' FormatPct(sumPct)
if hasJoe then call Emit 'Total JOEs    :' FormatInt(sumJoe)
if hasBert then call Emit 'Job BERTs     :' FormatInt(sumBert)
if hasJoeBert then
  call Emit 'JOE BERTs     :' FormatInt(sumJoeBert)

if matches = 0 then return

call Emit ''
call Emit 'TOP MATCHES BY TG PERCENT'
call Emit 'Jobname  JobID     Owner    Queue       TGs       TG%'
call Emit copies('-', 67)
call TopReset
do i = 1 to stRows
  if WildMatch(r_job.i, pat) then call TopAdd i, r_pct.i
end
do x = 1 to topCount
  i = topKey.x
  call Emit left(r_job.i,8) left(r_id.i,9) left(r_owner.i,8) ,
      left(r_queue.i,10) right(FormatInt(r_tg.i),9) ,
      right(FormatPct(r_pct.i),7)
end
return


ReportDetail:
parse upper arg pat
pat = strip(pat)
call Section 'JOB DATA SET DETAIL: 'pat

if \stAvail then do
  call Emit 'ST data unavailable.'
  return
end

found = 0
do i = 1 to stRows
  hit = 0
  if translate(r_id.i) = pat then hit = 1
  if WildMatch(r_job.i, pat) then hit = 1

  if hit & found < 3 then do
    found = found + 1
    call Emit ''
    call Emit 'Job:' r_job.i r_id.i 'Owner:' r_owner.i ,
        'Queue:' r_queue.i
    call Emit 'TG:' FormatInt(r_tg.i) ' TG%:' FormatPct(r_pct.i)

    isfcols2 = 'DDNAME STEPN PROCS DSID OCLASS DESTN RECCNT' ,
               'PAGECNT BYTECNT'
    Address SDSF "ISFACT ST TOKEN('"r_tok.i"') PARM(NP ?)" ,
                 "(PREFIX JDS_)"
    dRc = rc

    if dRc <> 0 then do
      call Emit 'Unable to obtain JDS rows for' r_id.i 'RC='dRc
      call SdsfMessages
    end
    else do
      drows = JDS_DDNAME.0
      call Emit 'SYSOUT data sets:' drows
      call Emit 'DDNAME    STEP      CLASS      RECORDS        BYTES'
      call Emit copies('-', 61)

      call TopReset
      do d = 1 to drows
        db = Num(JDS_BYTECNT.d)
        call TopAdd d, db
      end

      do x = 1 to topCount
        d = topKey.x
        dd = strip(JDS_DDNAME.d)
        stp = strip(JDS_STEPN.d)
        oc = strip(JDS_OCLASS.d)
        if dd = '' then dd = '-'
        if stp = '' then stp = '-'
        if oc = '' then oc = '-'
        call Emit left(dd,9) left(stp,9) left(oc,6) ,
            right(FormatInt(Num(JDS_RECCNT.d)),12) ,
            right(HumanBytes(Num(JDS_BYTECNT.d)),12)
      end
    end
  end
end

if found = 0 then call Emit 'No matching job/job ID found.'
if found = 3 then call Emit 'DETAIL limited to first 3 matching jobs.'
return


Findings:
call Section 'JES2 TRIAGE SUMMARY'

if sysAvail then
  call Emit 'Current JES SPOOL%       :' FormatPct(jesSpoolPct)
if hAvail then
  call Emit 'Held output groups       :' hRows
if oAvail then
  call Emit 'Non-held output groups   :' oRows
call Emit ''

obs = 0

act = ''
do z = 1 to words('TG JQE JNUM JOE BERT')
  res = word('TG JQE JNUM JOE BERT', z)
  if haspState.res = 'ACTIVE' then act = act res
end
act = strip(act)
if act <> '' then do
  obs = obs + 1
  call Emit obs'. ACTIVE $HASP050 resource pressure:' act
end

if stAvail then do
  /* Largest individual spool consumer. */
  maxPct = 0
  maxRow = 0
  do i = 1 to stRows
    if r_pct.i > maxPct then do
      maxPct = r_pct.i
      maxRow = i
    end
  end
  if maxPct >= groupPctWarn then do
    obs = obs + 1
    call Emit obs'. Large individual spool consumer:' ,
        r_job.maxRow r_id.maxRow FormatPct(maxPct)
  end

  /* Largest exact job-name aggregate. */
  maxPct = 0
  maxK = ''
  do x = 1 to jkeys.0
    k = jkeys.x
    if jpct.k > maxPct then do
      maxPct = jpct.k
      maxK = k
    end
  end
  if maxK <> '' & maxPct >= groupPctWarn then do
    obs = obs + 1
    call Emit obs'. Repeated job-name spool concentration:' jlabel.maxK ,
        jcnt.maxK 'jobs,' FormatPct(maxPct)
  end

  /* Largest related numeric-suffix family. */
  maxPct = 0
  maxK = ''
  do x = 1 to fkeys.0
    k = fkeys.x
    if fdist.k >= 2 & fpct.k > maxPct then do
      maxPct = fpct.k
      maxK = k
    end
  end
  if maxK <> '' & maxPct >= familyPctWarn then do
    obs = obs + 1
    call Emit obs'. Related-name spool family:' flabel.maxK ,
        fcnt.maxK 'jobs,' FormatPct(maxPct)
  end

  /* Large repeated job-name count, useful for JQE/JNUM pressure. */
  maxCnt = 0
  maxK = ''
  do x = 1 to jkeys.0
    k = jkeys.x
    if jcnt.k > maxCnt then do
      maxCnt = jcnt.k
      maxK = k
    end
  end
  if maxK <> '' & maxCnt >= repeatWarn then do
    obs = obs + 1
    call Emit obs'. High repeated job count:' jlabel.maxK ,
        maxCnt 'instances'
  end
end

/* Non-held OUTPUT class concentration. */
if oAvail & oRows > 0 then do
  maxCnt = 0
  maxK = ''
  do x = 1 to ockeys.0
    k = ockeys.x
    if occnt.k > maxCnt then do
      maxCnt = occnt.k
      maxK = k
    end
  end

  if maxK <> '' then do
    share = maxCnt * 100 / oRows
    if share >= classShareWarn then do
      obs = obs + 1
      call Emit obs'. Non-held SYSOUT class concentration:' oclabel.maxK ,
          maxCnt 'groups,' FormatPct(share)
    end
  end
end

if obs = 0 then call Emit 'No configured concentration threshold was exceeded.'
call Emit ''
call Emit 'Summary entries are diagnostic signals, not root-cause proof.'
return


/*====================================================================*/
/* RESOLUTION / DEBUGGING GUIDANCE                                    */
/*====================================================================*/

ResolutionGuide:
parse upper arg guideSel
showTG = 0
showJQE = 0
showJNUM = 0
showJOE = 0
showBERT = 0

if guideSel = 'AUTO' then do
  showTG = ResourceHot('TG')
  showJQE = ResourceHot('JQE')
  showJNUM = ResourceHot('JNUM')
  showJOE = ResourceHot('JOE')
  showBERT = ResourceHot('BERT')
end
else do
  if guideSel = 'TG' then showTG = 1
  if guideSel = 'JQE' then showJQE = 1
  if guideSel = 'JNUM' then showJNUM = 1
  if guideSel = 'JOE' then showJOE = 1
  if guideSel = 'BERT' then showBERT = 1
end

if showTG + showJQE + showJNUM + showJOE + showBERT = 0 then do
  call Section 'RESOLUTION / DEBUGGING GUIDANCE'
  call Emit 'No active or near-warning JES2 resource pressure detected.'
  call Emit 'No emergency capacity increase is indicated by this scan.'
  return
end

call Section 'RESOLUTION / DEBUGGING / EMERGENCY CAPACITY'
call Emit 'IMPORTANT SAFETY RULES'
call Emit '  - JOBDIAG is read-only. Commands below are guidance only.'
call Emit '  - Obtain site approval before any $T, $S, purge, or offload.'
call Emit '  - Verify current values and checkpoint capacity first.'
call Emit '  - Do NOT raise WARN percentages just to clear $HASP050.'
call Emit '    That suppresses warning; it does not add JES2 resources.'
call Emit '  - Change one capacity at a time, then re-display and verify.'
call Emit '  - Confirm resource usage falls and $HASP055/JRI clears.'
call Emit ''

if showTG then call GuideTG
if showJQE then call GuideJQE
if showJNUM then call GuideJNUM
if showJOE then call GuideJOE
if showBERT then call GuideBERT
return


ResourceHot:
procedure expose haspState. jriResAvail. jriPct. jriWarn. ,
                 jriShort. jriExh. sysAvail jesSpoolPct
parse upper arg rr

if haspState.rr = 'ACTIVE' then return 1

if jriResAvail.rr then do
  if jriShort.rr = 'YES' then return 1
  if jriExh.rr = 'YES' then return 1
  wp = jriWarn.rr
  if wp > 0 then do
    if jriPct.rr >= wp - 5 then return 1
  end
end

if rr = 'TG' & sysAvail then do
  if jesSpoolPct >= 80 then return 1
end
return 0


GuideUsage:
procedure expose jriResAvail. jriUse. jriMax. jriPct. jriWarn.
parse upper arg rr

if \jriResAvail.rr then return 'JRI usage unavailable'

s = FormatInt(jriUse.rr) || ' of ' || FormatInt(jriMax.rr)
s = s || ' used (' || FormatPct(jriPct.rr) || ')'
if jriWarn.rr > 0 then
  s = s || ' warn=' || FormatPct(jriWarn.rr)
return s


CapacityFloor:
procedure expose jriResAvail. jriUse. jriMax. jriWarn.
parse upper arg rr

if \jriResAvail.rr then return 0
if jriUse.rr <= 0 | jriMax.rr <= 0 then return 0

target = jriWarn.rr - 10
if target <= 0 then target = 70
if target < 50 then target = 50
if target > 85 then target = 85

need = trunc((jriUse.rr * 100 + target - 1) / target)
if need <= jriMax.rr then return 0

if rr = 'TG' then
  need = trunc((need + 16287) / 16288) * 16288
return need


GuideTG:
call Emit 'TG / SPOOL PRESSURE'
if jriResAvail.TG then
  call Emit '  JRI:' GuideUsage('TG')
if sysAvail then
  call Emit '  Current JES SPOOL%:' FormatPct(jesSpoolPct)

call Emit '  Debug / resolution:'
call Emit '    1. Review top TG jobs/families in JOBDIAG SPOOL.'
call Emit '    2. Review JOBDIAG OUTPUT and HELD for output backlog.'
call Emit '    3. Restore failed writer/SAPI/archive/report processing.'
call Emit '    4. Purge only confirmed disposable jobs/output.'
call Emit '    5. Consider JES2 spool offload if site procedures allow.'
call Emit '  Pre-check displays:'
call Emit '    $D SPOOL'
call Emit '    $D SPOOLDEF'
call Emit '    $D ACTIVATE'

floor = CapacityFloor('TG')
if floor > 0 then
  call Emit '  Advisory TGSPACE MAX floor for headroom:' FormatInt(floor)

call Emit '  Dynamic option ONLY if TGSPACE MAX is the constraint:'
call Emit '    $T SPOOLDEF,TGSPACE=(MAX=<approved-higher-value>)'
call Emit '  Critical: raising TGSPACE MAX does NOT create DASD spool.'
call Emit '  If physical spool is full, add/reactivate a verified spool'
call Emit '  volume with the site $S SPOOL procedure or use offload.'
call Emit '  TGSPACE MAX growth can temporarily affect performance while'
call Emit '  JES2 members read the changed checkpoint structures.'
call Emit '  Never FORMAT/add spool until VOLSER, DSNAME, affinity and'
call Emit '  existing contents are verified against the site procedure.'
call Emit ''
return


GuideJQE:
call Emit 'JQE - JOB QUEUE ELEMENT PRESSURE'
if jriResAvail.JQE then
  call Emit '  JRI:' GuideUsage('JQE')

call Emit '  Debug / resolution:'
call Emit '    1. Review repeated job names/families in JOBDIAG JQE.'
call Emit '    2. Check why completed jobs remain on JES queues.'
call Emit '    3. Check output/archive/reporting backlogs before purge.'
call Emit '    4. Purge only confirmed disposable jobs/output.'
call Emit '  Pre-check displays:'
call Emit '    $D JOBDEF'
call Emit '    $D CKPTSPACE'
call Emit '    $D ACTIVATE'

floor = CapacityFloor('JQE')
if floor > 0 then
  call Emit '  Advisory JOBNUM floor for headroom:' FormatInt(floor)

call Emit '  Emergency capacity template:'
call Emit '    $T JOBDEF,JOBNUM=<approved-higher-value>'
call Emit '  Verify: $D JOBDEF and $D CKPTSPACE.'
call Emit '  Warning: JOBNUM grows JQE capacity and can increase BERT'
call Emit '  demand/checkpoint requirements. It does NOT expand JNUM RANGE.'
call Emit ''
return


GuideJNUM:
call Emit 'JNUM - ASSIGNABLE JOB NUMBER PRESSURE'
if jriResAvail.JNUM then
  call Emit '  JRI:' GuideUsage('JNUM')

call Emit '  Debug / resolution:'
call Emit '    1. Use $D JOBDEF and inspect RANGE, JNUMFREE, JNUMWARN.'
call Emit '    2. Review repeated job submissions in JOBDIAG JQE.'
call Emit '    3. Stop the abnormal submission source if still growing.'
call Emit '  Dynamic option when the configured RANGE is too small:'
call Emit '    $T JOBDEF,RANGE=(<current-low>,<approved-new-high>)'
call Emit '  Warning: JOBNUM controls JQEs; RANGE controls assignable'
call Emit '  local JOB/STC/TSU numbers. They are different shortages.'
call Emit '  A RANGE maximum above 99999 changes JES2 JOBID format.'
call Emit '  Verify scripts/automation that assume JOBnnnnn first.'
call Emit '  Re-display with $D JOBDEF after the change.'
call Emit ''
return


GuideJOE:
call Emit 'JOE - JOB OUTPUT ELEMENT PRESSURE'
if jriResAvail.JOE then
  call Emit '  JRI:' GuideUsage('JOE')

call Emit '  Debug / resolution:'
call Emit '    1. Review JOBDIAG OUTPUT for dominant non-held classes.'
call Emit '    2. Review JOBDIAG HELD for held output accumulation.'
call Emit '    3. Check SAPI/archive/report writers and destinations.'
call Emit '    4. Purge only output confirmed no longer required.'
call Emit '  Pre-check displays:'
call Emit '    $D OUTDEF'
call Emit '    $D CKPTSPACE'
call Emit '    $D ACTIVATE'

floor = CapacityFloor('JOE')
if floor > 0 then
  call Emit '  Advisory JOENUM floor for headroom:' FormatInt(floor)

call Emit '  Emergency capacity template:'
call Emit '    $T OUTDEF,JOENUM=<approved-higher-value>'
call Emit '  Verify: $D OUTDEF and $D CKPTSPACE.'
call Emit '  Warning: increasing JOENUM can also increase BERT demand.'
call Emit '  Monitor BERT usage while the output backlog is cleared.'
call Emit ''
return


GuideBERT:
call Emit 'BERT - BLOCK EXTENSION REUSE TABLE PRESSURE'
if jriResAvail.BERT then
  call Emit '  JRI:' GuideUsage('BERT')

call Emit '  Debug / resolution:'
call Emit '    1. Review top BERT jobs/families in JOBDIAG BERT.'
call Emit '    2. Check JQE/JOE growth and output accumulation.'
call Emit '    3. Remove only confirmed stale jobs/output.'
call Emit '  Mandatory pre-check displays:'
call Emit '    $D CKPTSPACE'
call Emit '    $D CKPTSPACE,BERTUSE'
call Emit '    $D ACTIVATE'
call Emit '    $D JOBDEF'
call Emit '    $D OUTDEF'

floor = CapacityFloor('BERT')
if floor > 0 then
  call Emit '  Advisory BERTNUM floor for headroom:' FormatInt(floor)

call Emit '  Emergency capacity template:'
call Emit '    $T CKPTSPACE,BERTNUM=<approved-higher-value>'
call Emit '  Warning: BERT expansion consumes JES2 checkpoint space.'
call Emit '  If checkpoint capacity is insufficient, do not force it.'
call Emit '  Follow the site checkpoint expansion/reconfiguration process.'
call Emit '  Verify with $D CKPTSPACE and JRI after the change.'
call Emit ''
return

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

PrioritySection:
parse arg title, resource
call Emit ''
call Emit copies('-', 78)
if IsPriority(resource) then
  call Emit ' *** PRIORITY - $HASP050 *** 'title
else
  call Emit ' 'title
call Emit copies('-', 78)
return


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


IsPriority:
procedure expose haspState.
parse upper arg res
if res = 'SPOOL' then res = 'TG'
if haspState.res = 'ACTIVE' then return 1
return 0


TopReset:
topCount = 0
topKey. = ''
topVal. = -1
return


TopAdd:
parse arg newKey, newVal
newVal = Num(newVal)

insertAt = topCount + 1
do ti = 1 to topCount
  if newVal > topVal.ti then do
    insertAt = ti
    leave
  end
end

if insertAt <= topN then do
  newCount = topCount + 1
  if newCount > topN then newCount = topN

  do tj = newCount to insertAt + 1 by -1
    prev = tj - 1
    topKey.tj = topKey.prev
    topVal.tj = topVal.prev
  end

  topKey.insertAt = newKey
  topVal.insertAt = newVal
  topCount = newCount
end
return


FamilyOf:
procedure
parse upper arg n
n = strip(n)
ln = length(n)
if ln < 3 then return ''

sfx = right(n, 2)
if datatype(sfx, 'W') then return left(n, ln - 2)'##'
return ''


WildMatch:
procedure
parse upper arg value, pattern
value = strip(value)
pattern = strip(pattern)

if pattern = '*' then return 1

star = pos('*', pattern)
if star = 0 then return value = pattern

/* Support a single '*' anywhere: ABC*, *ABC, AB*CD. */
pre = left(pattern, star - 1)
post = substr(pattern, star + 1)

if pre <> '' then do
  if left(value, length(pre)) <> pre then return 0
end

if post <> '' then do
  if right(value, length(post)) <> post then return 0
end

if length(value) < length(pre) + length(post) then return 0
return 1


Num:
procedure
parse arg v
v = strip(v)
if v = '' then return 0

do while pos(',', v) > 0
  p = pos(',', v)
  v = delstr(v, p, 1)
end

do while pos(' ', v) > 0
  p = pos(' ', v)
  v = delstr(v, p, 1)
end

if right(v, 1) = '%' then v = left(v, length(v) - 1)
if datatype(v, 'N') then return v

u = translate(v)
mult = 1
cut = 0

if right(u, 2) = 'KB' then do
  mult = 1024
  cut = 2
end
else if right(u, 2) = 'MB' then do
  mult = 1024 * 1024
  cut = 2
end
else if right(u, 2) = 'GB' then do
  mult = 1024 * 1024 * 1024
  cut = 2
end
else if right(u, 2) = 'TB' then do
  mult = 1024 * 1024 * 1024 * 1024
  cut = 2
end
else if right(u, 1) = 'K' then do
  mult = 1000
  cut = 1
end
else if right(u, 1) = 'M' then do
  mult = 1000000
  cut = 1
end
else if right(u, 1) = 'G' then do
  mult = 1000000000
  cut = 1
end

if cut > 0 then do
  n = left(v, length(v) - cut)
  if datatype(n, 'N') then return n * mult
end
return 0


FormatInt:
procedure
parse arg n
n = Num(n)
return strip(format(n, 12, 0))


FormatPct:
procedure
parse arg n
n = Num(n)
return strip(format(n, 7, 2))'%'


CountPct:
procedure
parse arg n,p
cnt = FormatInt(n)
pct = FormatPct(p)
return cnt || ' (' || pct || ')'


DisplayDate:
procedure
d = date('S')
yyyy = left(d,4)
mm = substr(d,5,2) + 0
dd = substr(d,7,2)
months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'
mmm = word(months,mm)
return dd || '-' || mmm || '-' || yyyy


HumanBytes:
procedure
parse arg n
n = Num(n)
if n < 1024 then return FormatInt(n)' B'
if n < 1024 * 1024 then
  return strip(format(n / 1024, 7, 1))' KB'
if n < 1024 * 1024 * 1024 then
  return strip(format(n / (1024 * 1024), 7, 1))' MB'
if n < 1024 * 1024 * 1024 * 1024 then
  return strip(format(n / (1024 * 1024 * 1024), 7, 1))' GB'
return strip(format(n / (1024 * 1024 * 1024 * 1024), 7, 1))' TB'


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


SampleReport:
sampleSys = mvsvar('SYSNAME')

call Emit ''
call Emit copies('=', 78)
call Emit ' JOBDIAG - JES2 RESOURCE TRIAGE'
call Emit ' Author: SARATH S'
call Emit ' System:' sampleSys
call Emit ' Date:' DisplayDate() ' Time:' time('N') ' Mode: SAMPLE'
call Emit copies('=', 78)
call Emit ''
call Emit ' *** SAMPLE REPORT - DEMONSTRATION DATA ONLY ***'
call Emit ''
call Emit 'All jobs, counts, percentages, classes and shortage values below'
call Emit 'are synthetic examples. The system name/date/time are current.'
call Emit 'Display and modify-command syntax is operational guidance.'
call Emit 'Site-specific values remain placeholders and must be verified.'
call Emit ''

call Section 'JES2 TRIAGE SUMMARY'
call Emit 'Current JES SPOOL%       : 86.00%'
call Emit 'Held output groups       : 18342'
call Emit 'Non-held output groups   : 24781'
call Emit ''
call Emit '1. ACTIVE $HASP050 resource pressure: TG JQE'
call Emit '2. Large individual spool consumer: BIGRPT01 JOB18425 18.40%'
call Emit '3. Repeated job-name spool concentration: ARCHJOB 642 jobs, 8.60%'
call Emit '4. Related-name spool family: DLYRPT## 219 jobs, 22.70%'
call Emit '5. High repeated job count: ARCHJOB 642 instances'
call Emit '6. Non-held SYSOUT class concentration: K 20184 groups, 81.45%'
call Emit ''
call Emit 'INTERPRETATION'
call Emit 'The sample indicates both TG/SPOOL and JQE pressure.'
call Emit 'A DLYRPT numeric-suffix family is the largest collective spool'
call Emit 'consumer, while class K output is heavily accumulated.'
call Emit 'This pattern is consistent with an output/archive backlog plus'
call Emit 'large retained report output. Confirm the downstream processor'
call Emit 'before deleting output or increasing JES2 capacity.'
call Emit ''

call Section 'RESOLUTION / DEBUGGING / EMERGENCY CAPACITY'
call Emit 'IMPORTANT SAFETY RULES'
call Emit '  - JOBDIAG is read-only. Commands below are guidance only.'
call Emit '  - Obtain site approval before any $T, $S, purge, or offload.'
call Emit '  - Verify current values and checkpoint capacity first.'
call Emit '  - Do NOT raise WARN percentages just to clear $HASP050.'
call Emit '    That changes warning timing; it does not add capacity.'
call Emit '  - Correct the backlog/root cause even if capacity is raised.'
call Emit ''
call Emit 'TG / SPOOL PRESSURE - DEBUG'
call Emit '  Display commands:'
call Emit '    $D SPOOL'
call Emit '    $D SPOOLDEF'
call Emit '    $D ACTIVATE'
call Emit '  JOBDIAG follow-up:'
call Emit '    TSO JOBDIAG SPOOL'
call Emit '    TSO JOBDIAG OUTPUT K'
call Emit '    TSO JOBDIAG HELD K'
call Emit '  Possible emergency option when TGSPACE MAX is limiting:'
call Emit '    $T SPOOLDEF,TGSPACE=(MAX=<approved-higher-value>)'
call Emit '  Warning: this does NOT create physical DASD spool capacity.'
call Emit '  If spool volumes are physically full, free/offload output or'
call Emit '  follow the site procedure to add/reactivate a spool volume.'
call Emit ''
call Emit 'JQE PRESSURE - DEBUG'
call Emit '  Display commands:'
call Emit '    $D JOBDEF'
call Emit '    $D CKPTSPACE'
call Emit '    $D ACTIVATE'
call Emit '  JOBDIAG follow-up:'
call Emit '    TSO JOBDIAG JQE'
call Emit '  Possible emergency capacity option:'
call Emit '    $T JOBDEF,JOBNUM=<approved-higher-value>'
call Emit '  Warning: JOBNUM growth can increase BERT/checkpoint demand.'
call Emit ''
call Emit 'JNUM PRESSURE - IF IDENTIFIED'
call Emit '  Display command:'
call Emit '    $D JOBDEF'
call Emit '  Possible range expansion:'
call Emit '    $T JOBDEF,RANGE=(<current-low>,<approved-new-high>)'
call Emit '  Warning: RANGE and JOBNUM solve different resource limits.'
call Emit ''
call Emit 'JOE PRESSURE - IF IDENTIFIED'
call Emit '  Display commands:'
call Emit '    $D OUTDEF'
call Emit '    $D CKPTSPACE'
call Emit '    $D ACTIVATE'
call Emit '  Possible emergency capacity option:'
call Emit '    $T OUTDEF,JOENUM=<approved-higher-value>'
call Emit '  Warning: JOENUM growth can increase BERT demand.'
call Emit ''
call Emit 'BERT PRESSURE - IF IDENTIFIED'
call Emit '  Mandatory pre-check displays:'
call Emit '    $D CKPTSPACE'
call Emit '    $D CKPTSPACE,BERTUSE'
call Emit '    $D ACTIVATE'
call Emit '    $D JOBDEF'
call Emit '    $D OUTDEF'
call Emit '  Possible emergency capacity option:'
call Emit '    $T CKPTSPACE,BERTNUM=<approved-higher-value>'
call Emit '  Warning: BERT expansion requires checkpoint capacity.'
call Emit ''

call Section 'RESOURCE / DATA SUMMARY'
call Emit ' JES SPOOL%            : 86.00%'
call Emit ' JES/system            : JES2' sampleSys 'JES2'
call Emit ' JQEs                  : 18420 (92.10%)'
call Emit ' JOEs                  : 22650 (75.50%)'
call Emit ' BERTs                 : 14580 (72.90%)'
call Emit ' Held output groups    : 18342'
call Emit ' Non-held output groups: 24781'
call Emit ' SDSF SYSNAME filter   : * (all authorized systems)'
call Emit ''

call Section 'SPOOL / TRACK GROUP ANALYSIS'
call Emit 'CURRENT SPOOL STATE'
call Emit 'Current JES SPOOL%        : 86.00%'
call Emit 'Visible ST job TG%        : 64.20%'
call Emit 'Other/unattributed TG%    : 21.80%'
call Emit ''
call Emit 'TOP INDIVIDUAL JOBS BY TG PERCENT'
call Emit 'Jobname  JobID     Owner    Queue       TGs       TG%'
call Emit copies('-', 67)
call Emit 'BIGRPT01 JOB18425  RPTUSER  PRINT      18420    18.40%'
call Emit 'SYSLOG   STC02341  STCUSER  EXEC        6310     6.30%'
call Emit 'DLYRPT20 JOB18021  BATCH1   PRINT        448     0.45%'
call Emit 'DLYRPT30 JOB18102  BATCH1   PRINT        441     0.44%'
call Emit ''
call Emit 'TOP EXACT JOBNAME AGGREGATES BY TG PERCENT'
call Emit 'Jobname  Jobs       TGs        TG%  Flag'
call Emit copies('-', 55)
call Emit 'ARCHJOB    642     8610      8.60%  <-- SIGNIFICANT'
call Emit 'BIGRPT01     1    18420     18.40%  <-- SIGNIFICANT'
call Emit ''
call Emit 'NUMERIC-SUFFIX JOB FAMILIES'
call Emit 'Family    Names  Jobs       TGs        TG%  Flag'
call Emit copies('-', 62)
call Emit 'DLYRPT##      5   219     22700     22.70%  <-- SIGNIFICANT'
call Emit ''

call Section 'JOB QUEUE / JQE / JNUM ANALYSIS'
call Emit 'Current JQEs : 18420 (92.10%)'
call Emit ''
call Emit 'JOBS BY JES QUEUE'
call Emit 'Queue       Jobs      Share'
call Emit copies('-', 32)
call Emit 'PRINT      28620     73.40%'
call Emit 'EXECUTION    302      0.77%'
call Emit 'INPUT       1840      4.72%'
call Emit ''
call Emit 'MOST REPEATED EXACT JOBNAMES'
call Emit 'Jobname    Instances   TG%'
call Emit copies('-', 34)
call Emit 'ARCHJOB          642    8.60%'
call Emit 'DLYRPT20          48    5.10%'
call Emit 'DLYRPT30          46    4.80%'
call Emit ''

call Section 'JOE ANALYSIS'
call Emit 'Current JOEs : 22650 (75.50%)'
call Emit ''
call Emit 'TOP EXACT JOBNAME AGGREGATES BY JOEs'
call Emit 'Jobname  Jobs       JOEs'
call Emit copies('-', 34)
call Emit 'ARCHJOB    642       7820'
call Emit 'DLYRPT20    48       2410'
call Emit 'DLYRPT30    46       2294'
call Emit ''

call Section 'BERT ANALYSIS'
call Emit 'Current BERTs : 14580 (72.90%)'
call Emit ''
call Emit 'TOP EXACT JOBNAME AGGREGATES BY JOB BERTs'
call Emit 'Jobname  Jobs      BERTs'
call Emit copies('-', 34)
call Emit 'ARCHJOB    642       3380'
call Emit 'BIGRPT01     1       2170'
call Emit ''

call Section 'HELD OUTPUT ANALYSIS'
call Emit 'Held output groups visible: 18342'
call Emit ''
call Emit 'TOP HELD OUTPUT CLASSES BY OUTPUT-GROUP COUNT'
call Emit 'Class       Groups      Share       Records  Flag'
call Emit copies('-', 62)
call Emit 'K             9320      50.81%      78200410  <-- CONCENTRATION'
call Emit 'A             4110      22.41%      18220613'
call Emit ''
call Emit 'TOP JOBNAMES IN HELD OUTPUT'
call Emit 'Jobname     Groups       Records'
call Emit copies('-', 39)
call Emit 'ARCHJOB        5120       45211320'
call Emit 'DLYRPT20       1884       13801004'
call Emit ''

call Section 'NON-HELD OUTPUT ANALYSIS'
call Emit 'Non-held output groups visible: 24781'
call Emit ''
call Emit 'TOP NON-HELD OUTPUT CLASSES'
call Emit 'Class       Groups      Share       Records  Flag'
call Emit copies('-', 62)
call Emit 'K            20184      81.45%     182394201  <-- CONCENTRATION'
call Emit 'A             2210       8.92%      18482111'
call Emit ''
call Emit 'TOP JOBNAMES IN NON-HELD OUTPUT'
call Emit 'Jobname     Groups       Records'
call Emit copies('-', 39)
call Emit 'ARCHJOB        8231       88421902'
call Emit 'DLYRPT20       4117       38210011'
call Emit 'DLYRPT30       3981       36188442'
call Emit ''
call Emit 'TOP EXTERNAL WRITERS'
call Emit 'Writer       Groups'
call Emit copies('-', 25)
call Emit 'RPTARCH       18831'
call Emit 'LOCAL          1982'
call Emit ''

call Section 'SAMPLE CONCLUSION'
call Emit 'Likely sample scenario:'
call Emit '  A reporting/archive path handling SYSOUT class K is not'
call Emit '  draining normally. Large repeated report jobs are accumulating'
call Emit '  and collectively consuming significant TG/JQE/JOE resources.'
call Emit ''
call Emit 'Suggested operator sequence:'
call Emit '  1. Confirm $HASP050/JRI resource pressure.'
call Emit '  2. Identify the dominant job/class/family using JOBDIAG.'
call Emit '  3. Check the relevant output writer/archive/reporting process.'
call Emit '  4. Restore processing or remove confirmed disposable output.'
call Emit '  5. Use an approved dynamic capacity increase only to buy time.'
call Emit '  6. Re-run JOBDIAG and confirm usage is falling.'
call Emit ''
call Emit 'This SAMPLE mode performs no JES2/SDSF diagnostic scan and makes'
call Emit 'no system changes. Only current system/date/time are live values.'
call Emit ''

call Footer
return


ShowHelp:
call Emit ''
call Emit copies('=', 78)
call Emit ' JOBDIAG - JES2 RESOURCE DIAGNOSTIC UTILITY'
call Emit ' Author: SARATH S'
call Emit copies('=', 78)
call Emit ''
call Emit 'WHAT IS JOBDIAG?'
call Emit ''
call Emit 'JOBDIAG is a read-only JES2 diagnostic utility for identifying'
call Emit 'the likely cause of JES2 resource pressure without manually'
call Emit 'working through multiple SDSF panels.'
call Emit ''
call Emit 'It collects authorized information from SDSF and summarizes'
call Emit 'where JES2 spool, job queue and output resources are being used.'
call Emit 'It is intended for incident triage, not automatic remediation.'
call Emit ''
call Emit 'WHAT DOES IT DIAGNOSE?'
call Emit ''
call Emit '  - JES SPOOL / Track Group usage and major consumers'
call Emit '  - Repeated copies of the same job consuming resources'
call Emit '  - Related numeric-suffix job families such as XXXXXD20/D30'
call Emit '  - JQE and JNUM pressure and abnormal job accumulation'
call Emit '  - JOE usage and output-group accumulation'
call Emit '  - BERT usage by jobs and JOEs'
call Emit '  - Held SYSOUT concentration by class, job and hold reason'
call Emit '  - Non-held SYSOUT concentration by class, job and writer'
call Emit '  - Large SYSOUT DDs for a selected job'
call Emit '  - Current/ recent $HASP050 JES2 resource-shortage conditions'
call Emit ''
call Emit 'HOW DOES IT WORK?'
call Emit ''
call Emit 'JOBDIAG uses the supported SDSF REXX interface. Depending on'
call Emit 'the requested function it reads SDSF SYS, JRI, ST, H, O, JDS'
call Emit 'and SYSLOG information that the executing user is authorized'
call Emit 'to see. It does not scrape SDSF screens.'
call Emit ''
call Emit 'The default JOBDIAG or JOBDIAG ALL run produces a JES2 triage'
call Emit 'summary first, followed by resource-specific analysis and'
call Emit 'resolution/debugging guidance when pressure is detected.'
call Emit ''
call Emit 'TSO JOBDIAG SAMPLE generates a demonstration report using'
call Emit 'synthetic incident data. It lets users see the report layout,'
call Emit 'interpretations, debugging steps and advisory commands before'
call Emit 'they need to run JOBDIAG during a real JES2 incident.'
call Emit ''
call Emit 'SAFETY'
call Emit ''
call Emit '  - JOBDIAG is read-only.'
call Emit '  - It never purges, releases, cancels or holds jobs/output.'
call Emit '  - It never issues $T, $S or other modifying JES2 commands.'
call Emit '  - Any dynamic command shown in the report is advisory only.'
call Emit '  - Verify current values and follow site change/approval rules'
call Emit '    before using any suggested JES2 capacity command.'
call Emit ''
call Emit 'REPORT OUTPUT'
call Emit ''
call Emit 'When run under ISPF, JOBDIAG opens the completed report in'
call Emit 'ISPF Browse so F7/F8 scrolling, FIND and F9 SWAP work normally.'
call Emit ''
call Emit 'Each successful report is also retained as a cataloged data set:'
call Emit ''
call Emit '  <USERID>.JOBDIAG.Dyymmdd.Thhmmss'
call Emit ''
call Emit 'Example:'
call Emit '  TSSSK.JOBDIAG.D260819.T221530'
call Emit ''
call Emit 'The saved data set remains available after exiting Browse and'
call Emit 'can be attached to an incident, compared later or deleted when'
call Emit 'it is no longer required.'
call Emit ''
call Emit 'IMPORTANT LIMITATIONS'
call Emit ''
call Emit '  - Results depend on the caller SDSF/SAF authorization.'
call Emit '  - SYSNAME=* requests all systems the caller may access.'
call Emit '  - Counts may therefore be lower than unrestricted JES totals.'
call Emit '  - JOBDIAG identifies strong diagnostic signals; concentration'
call Emit '    does not by itself prove the root cause.'
call Emit '  - Numeric-suffix family detection currently groups only job'
call Emit '    names ending in two digits.'
call Emit '  - Dynamic capacity suggestions buy time; they do not replace'
call Emit '    correction of the actual backlog or failing process.'
call Emit ''
call Emit 'RECOMMENDED FIRST USE'
call Emit ''
call Emit '  1. Run TSO JOBDIAG ALL for a complete JES2 snapshot.'
call Emit '  2. Read the JES2 TRIAGE SUMMARY at the top.'
call Emit '  3. Review the emphasized resource-specific section.'
call Emit '  4. Use a targeted command for deeper analysis if required.'
call Emit '  5. Validate the suggested cause before taking corrective action.'
call Emit ''
call Emit 'COMMAND REFERENCE'
call Emit ''
call Emit '  TSO JOBDIAG'
call Emit '      Complete JES2 resource triage. Same as JOBDIAG ALL.'
call Emit ''
call Emit '  TSO JOBDIAG ALL'
call Emit '      Full spool, queue, JOE, BERT, held and output analysis.'
call Emit ''
call Emit '  TSO JOBDIAG SPOOL'
call Emit '      JES spool/TG usage, top jobs, repeated jobs and families.'
call Emit ''
call Emit '  TSO JOBDIAG TG'
call Emit '      Alias of JOBDIAG SPOOL.'
call Emit ''
call Emit '  TSO JOBDIAG JQE'
call Emit '      JQE-oriented queue and repeated-job analysis.'
call Emit ''
call Emit '  TSO JOBDIAG JNUM'
call Emit '      JNUM-oriented job-number pressure analysis.'
call Emit ''
call Emit '  TSO JOBDIAG JOE'
call Emit '      JOE usage and major JOE consumers.'
call Emit ''
call Emit '  TSO JOBDIAG BERT'
call Emit '      Job and JOE BERT usage analysis.'
call Emit ''
call Emit '  TSO JOBDIAG HELD'
call Emit '      Analyze all held SYSOUT.'
call Emit ''
call Emit '  TSO JOBDIAG HELD K'
call Emit '      Analyze only held SYSOUT class K.'
call Emit ''
call Emit '  TSO JOBDIAG OUTPUT'
call Emit '      Analyze all non-held output.'
call Emit ''
call Emit '  TSO JOBDIAG OUTPUT K'
call Emit '      Analyze only non-held SYSOUT class K.'
call Emit ''
call Emit '  TSO JOBDIAG OUT K'
call Emit '      Alias of JOBDIAG OUTPUT K.'
call Emit ''
call Emit '  TSO JOBDIAG CLASS K'
call Emit '      Combined held + non-held SYSOUT class K analysis.'
call Emit ''
call Emit '  TSO JOBDIAG JOB ABC*'
call Emit '      Analyze jobs matching a job-name pattern.'
call Emit ''
call Emit '  TSO JOBDIAG DETAIL JOB12345'
call Emit '      Show the largest SYSOUT DDs for the selected job ID.'
call Emit ''
call Emit '  TSO JOBDIAG SAMPLE'
call Emit '      Generate a full demonstration report with sample data.'
call Emit '      Current system/date/time are real; incident data is not.'
call Emit ''
call Emit '  TSO JOBDIAG HELP'
call Emit '      Display this built-in README and command reference.'
call Emit ''
call Emit copies('=', 78)
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


PresentReport:
procedure expose rpt. rptCount ispfActive
/* 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')

/* Keep every successful report as a unique cataloged data set. */
dqual = 'D'
dqual = dqual || substr(date('S'),3,6)

tqual = 'T'
tval = time('N')
tqual = tqual || substr(tval,1,2)
tqual = tqual || substr(tval,4,2)
tqual = tqual || substr(tval,7,2)

rptDsn = uid'.JOBDIAG.'dqual'.'tqual
ddn = 'JDRPT001'

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
  say 'JOBDIAG: Unable to allocate saved report data set.'
  do pi = 1 to rptCount
    say rpt.pi
  end
  return
end

/* Record the saved location inside the report itself. */
rptCount = rptCount + 1
rpt.rptCount = ''
rptCount = rptCount + 1
rpt.rptCount = "Saved report dataset : '"rptDsn"'"
rpt.0 = rptCount

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"'"
  say 'JOBDIAG: Report write failed; incomplete data set deleted.'
  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"

if bRc > 4 then do
  say 'JOBDIAG: ISPF Browse failed RC='bRc
  say "Saved report remains cataloged as '"rptDsn"'"
end
return


Footer:
call Emit ''
call Emit copies('=', 78)
call Emit ' JOBDIAG completed. No JES2 resources or jobs were modified.'
call Emit copies('=', 78)
return


Cleanup:
if sdsfOn = 1 then do
  rc = isfcalls('OFF')
  sdsfOn = 0
end
return


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