Skip to main content

User Key Common Flags in EasySMF

As you have probably heard, user key common storage will not be allowed in z/OS 2.4.

For more information on z/OS 2.4 User Key Common removal see Marna Walle’s article: Reminder to take a look: z/OS V2.4 user key common removal

IBM added some flags in the type 30 SMF record to audit usage of user key common storage. The flags allow you to see which jobs used user key common storage. The flags are:

  • SMF30_UserKeyCsaUsage
  • SMF30_UserKeyCadsUsage
  • SMF30_UserKeyChangKeyUsage

There is another flag for Restricted Use Common Service Area:

  • SMF30_UserKeyRuCsaUsage

Restricted use CSA is a relatively new function not going away in 2.4, but IBM discourage its use and it is becoming a priced feature.

EasySMF provides reports to show these flags and help you find any jobs or address spaces using user key common storage. The common storage flags are shown in the following reports:

  • Job Memory Information – Shows information from jobs and address spaces after they have ended.
  • Step Completions – Shows information from ended steps. This report shows the program name.
  • Running Jobs – Shows information including jobs that are still running (from SMF type 30 interval records).

You will need to scroll right to find the User Key Common columns.

EasySMF User Key Common Report

  • User Key Audit indicates whether the SMF30_UserKeyCommonAuditEnabled flag is set. This must be on, otherwise the information in the other fields is not valid.
  • User Key CSA shows the value of SMF30_UserKeyCsaUsage.
  • User Key CADS shows the value of SMF30_UserKeyCadsUsage
  • User Key CHANGKEY shows the value of SMF30_UserKeyChangKeyUsage
  • Restricted Use CSA shows the value of SMF30_UserKeyRuCsaUsage

Click the column headers to sort the values and find any jobs where the flags are set (click twice to sort descending).

User Key Common reports on z/OS using Java

If you don’t want to download SMF data to a PC, you can run a report on z/OS using Java. EasySMF:JE (a set of Java classes to map SMF records) provides a sample report to show User Key Common information.

Quickstart installation information for EasySMF:JE can be found here: EasySMF:JE Java Quickstart

Change the class name in IVP4 from com/blackhillsoftware/samples/RecordCount to com/blackhillsoftware/samples/UserKeyCommon to run the User Key Common sample report.

30 Day Trial

Both EasySMF and EasySMF:JE can be downloaded for 30 day trials.

Information about the trial is available here.

Understanding z/OS Unix Work with EasySMF

Unix work running under z/OS can be difficult to track.

A Unix process can create thousands of child processes, each running in another address space. They may only exist for fractions of a second and produce no job output, so you don’t see them in SDSF. Child processes create their own SMF type 30 records for job accounting, so their resource usage doesn’t appear in the parent’s accounting records. The child processes may have a different jobname and can even run in different service and report classes to the parent.

I have seen a job running BPXBATCH where the job itself used virtually no CPU time, but it spawned over 10,000 sub tasks. More than 99% of the CPU time used by that batch job appeared in child process SMF records with different job names, service and report classes.

EasySMF has reports to help you understand your z/OS Unix work.

EasySMF uses the Unix process and parent process id information from the type 30 SMF records to build a tree view of your Unix work.

  • You can see the parent – child relationships between different address spaces.
  • You can see whether the child service and report classes are the same as the original job.
  • When you collapse the tree, usage information from all the children is rolled up into the parent job. When you expand the tree, usage information is shown for the individual entries.

The expanded view of Unix work, showing the relationship between work running in different address spaces:

The collapsed view of the same work. Resources like CPU time show the total for all the related address spaces:

EasySMF can also help you find where work came from. If Related Processes is selected, EasySMF will search for and show parent and child tasks that do not match the main selection criteria. Here we can see a task with the job name ANDREWR, and Related Processes shows that particular task came in through SSH, and it had a number of Unix sub tasks.

Related Processes can help you find out where a Unix process came from.

More Detailed Information about Unix Work

The Unix Work report shows you more detail about work that uses z/OS Unix. As the name suggests, it only shows work that had a Unix component – other batch jobs and started tasks are ignored. This report shows information from the step and substep end records, which include some information about the program that was executed. This give clues to the processing taking place, although the information included in SMF is limited and may not show every program.

The Unix Work report shows information from Step End records for work with a Unix component, including the program information recorded in SMF.

Unix processes don’t always run in a different address space under z/OS. Sometimes processes will share an address space. This gives multiple Unix Process sections in SMF, with most of the statistics reported at the shared address space level. You can see this in the Unix Process report where you have processes that do not report the address space level information.

When multiple processes share an address space only one will show address space level information.

You can filter the Unix Work report to find a particular process ID – in this case the selected process was one of multiple processes that shared one address space.

Detailed Unix information is included in EasySMF since version 3.2.

Upgrades are free for existing customers. If you are not a customer, a 30 day trial is available.

Java mapping for CICS SMF records

The EasySMF Java API now has experimental support for CICS records.

Experimental, because I want to get some feedback from CICS users about class names, usage etc. before locking down the design. In particular:

  • Do the class names and organization make sense to a CICS person? Would other names or a different organization make more sense?
  • Are the examples of how to process data clear and useful?
  • Are there areas where terminology is used incorrectly?

The complete Javadoc is here with an overview of the CICS functionality here.

If you have any comments, you can leave feedback in the comments box below, send it to support@blackhillsoftware.com, or give feedback in person at booth 323 at SHARE in Providence, Rhode Island.

You can try out the API using the 30 day trial available here: 30 Day Trial.

Installation information is available here: EasySMF:JE Java Quickstart

Using the API

EasySMF:JE aims to provide a consistent interface across different SMF record types and sections, and converts values to standard Java types for simple programming.

Dates and Times

Dates and times are converted to java.time classes. Java.time can represent dates and times with a precision of 1 nanosecond.

Times representing a duration e.g. CPU or elapsed time are converted to Duration.
Dates and times of day are converted to LocalDate, LocalTime, LocalDateTime or ZonedDateTime depending on exactly what information is in the field. Typically, times based on UTC(GMT) are converted to ZonedDateTime with ZoneOffset.UTC. Other dates and times are converted to LocalDate/Times.
Java has time zone rules so it is possible to apply a ZoneId to a LocalDateTime and perform date aware conversions between time zones.

Numeric Values

1, 2 and 3 byte integer values and 4 byte signed integer values are converted to int (32 bit signed) values.
4-7 byte integer values and 8 byte signed values are converted to long (64 bit signed).

8 byte unsigned values are available as both long (64 bit signed) and as a BigInteger. The long value may provide better performance if the value will not exceed the maximum value for a long. If a value does exceed the maximum value (i.e. the high order bit is set) an exception will be thrown. If the field value might exceed the maximum value for a long, use the BigInteger version.

Integer values greater than 8 bytes are converted to BigInteger.

Floating point values are converted to Java double.

String Values

EBCDIC and UTF8 string/character values are converted to String. Java uses Unicode internally – values are converted from EBCDIC or UTF8.

Flags

Flag bits within a byte are converted to a boolean value indicating whether the bit is set.

CICS Statistics

Reading CICS statistics is very done the same way as reading sections from other records using the API. Sections of a specific type are returned in a List<E> of that type. If there are no sections of the type in the record an empty List is returned. This allows you to iterate over the sections without explicitly checking whether the sections exist in the record – an empty list will iterate 0 times.

Example

The following code reads all FileControlStatistics sections from type 110 SMF records from the DD INPUT.

try (SmfRecordReader reader =
SmfRecordReader
.fromDD("INPUT")
.include(110, Smf110Record.SMFSTSTY))
{
for (SmfRecord record : reader)
{
Smf100Record r110 = new Smf110Record(record);
for (FileControlStatistics fc :
r110.fileControlStatistics())
{
//... process FileControlStatistics sections here
}
}
}

CICS Performance Monitoring

Accessing data from CICS monitoring performance records is slightly different to other SMF records because the data needs to be accessed using a Dictionary.

Dictionary records are handled automatically, however you cannot access the data from a record before a related dictionary record has been seen. You can check whether a dictionary record is available using Smf110Record.haveDictionary() or simply concatenate all required dictionary records ahead of the data records in the input data.

Specific fields are defined by name and type. Then Performance records are read from the SMF record, and specific fields accessed using getField(…) methods or variations.

Example

ByteStringField transactionField = ByteStringField.define("DFHTASK","C001");
TimestampField startField = TimestampField.define("DFHCICS","T005");
TimestampField stopField = TimestampField.define("DFHCICS","T006");
ClockField dispatchField = ClockField.define("DFHTASK","S007");

try (SmfRecordReader reader =
SmfRecordReader
.fromDD("INPUT")
.include(110, Smf110Record.SMFMNSTY))
{
for (SmfRecord record : reader)
{
Smf100Record r110 = new Smf110Record(record);
if (r110.haveDictionary())
{
for (PerformanceRecord perfdata :
r110.performanceRecords())
{
String txName = perfdata.getField(transactionField);
ZonedDateTime start = perfdata.getField(startField);
ZonedDateTime stop = perfdata.getField(stopField);
double dispatch = perfdata.getFieldTimerSeconds(dispatchField);

//... process data
}
}
}
}

Complete CICS Statistics reporting sample

These samples are designed to show how to use the API, not to suggest items that you should specifically be reporting. However comments about their relevance are welcome.

import java.io.*;
import java.util.*;
import static java.util.Comparator.comparing;

import com.blackhillsoftware.smf.*;
import com.blackhillsoftware.smf.cics.*;
import com.blackhillsoftware.smf.cics.statistics.FileControlStatistics;

public class CicsFileStatistics
{
public static void main(String[] args) throws IOException
{
Map<String, Map<String, FileData>> applids =
new HashMap<String, Map<String, FileData>>();

try (SmfRecordReader reader =
args.length == 0 ?
SmfRecordReader.fromDD("INPUT") :
SmfRecordReader.fromStream(new FileInputStream(args[0])))
{
reader.include(110, Smf110Record.SMFSTSTY);
for (SmfRecord record : reader)
{
Smf110Record r110 = new Smf110Record(record);

Map<String, FileData> applidFiles =
applids.computeIfAbsent(r110.stProductSection().smfstprn(),
files -> new HashMap<String, FileData>());

for (FileControlStatistics fileStats : r110.fileControlStatistics())
{
String entryName = fileStats.a17fnam();
applidFiles.computeIfAbsent(entryName,
x -> new FileData(entryName)).add(fileStats);
}
}
}
writeReport(applids);
}

private static void writeReport(Map<String, Map<String, FileData>> applidFiles)
{

applidFiles.entrySet().stream()
.filter(applid -> !applid.getValue().isEmpty())
.sorted((a, b) -> a.getKey().compareTo(b.getKey()))
.forEachOrdered(applid ->
{
// Headings
System.out.format("%n%-8s", applid.getKey());

System.out.format("%n%-8s %12s %12s %12s %12s %12s %12s %12s %12s%n%n",
"ID",
"Gets",
"Get Upd",
"Browse",
"Adds",
"Updates",
"Deletes",
"Data EXCP",
"Index EXCP");

applid.getValue().entrySet().stream()
.map(x -> x.getValue())
.sorted(comparing(FileData::getTotalExcps)
.reversed())
.forEachOrdered(fileInfo ->
{
// write detail line
System.out.format("%-8s %12d %12d %12d %12d %12d %12d %12d %12d%n",
fileInfo.getId(),
fileInfo.getGets(),
fileInfo.getGetUpd(),
fileInfo.getBrowse(),
fileInfo.getAdds(),
fileInfo.getUpdates(),
fileInfo.getDeletes(),
fileInfo.getDataExcps(),
fileInfo.getIndexExcps());
});
});

}

private static class FileData
{
public FileData(String fileId)
{
this.id = fileId;
}

public void add(FileControlStatistics fileStatistics)
{
gets += fileStatistics.a17dsrd();
getupd += fileStatistics.a17dsgu();
browse += fileStatistics.a17dsbr();
add = fileStatistics.a17dswra();
update = fileStatistics.a17dswru();
delete = fileStatistics.a17dsdel();
dataexcp = fileStatistics.a17dsxcp();
indexexcp = fileStatistics.a17dsixp();
totalexcp += fileStatistics.a17dsxcp()
+ fileStatistics.a17dsixp();
}

public String getId()
{
return id;
}

public long getGets()
{
return gets;
}

public long getGetUpd()
{
return getupd;
}

public long getBrowse()
{
return browse;
}

public long getAdds()
{
return add;
}

public long getUpdates()
{
return update;
}

public long getDeletes()
{
return delete;
}

public long getDataExcps()
{
return dataexcp;
}

public long getIndexExcps()
{
return indexexcp;
}

public long getTotalExcps()
{
return totalexcp;
}

private String id;
private long gets = 0;
private long getupd = 0;
private long browse = 0;
private long add = 0;
private long update = 0;
private long delete = 0;
private long dataexcp = 0;
private long indexexcp = 0;
private long totalexcp = 0;
}
}

Complete CICS Transaction Monitoring reporting sample

import java.io.*;
import java.time.*;
import java.util.*;
import static java.util.Collections.reverseOrder;
import static java.util.Comparator.comparing;

import com.blackhillsoftware.smf.*;
import com.blackhillsoftware.smf.cics.*;
import com.blackhillsoftware.smf.cics.monitoring.*;
import com.blackhillsoftware.smf.cics.monitoring.fields.*;

public class CicsTransactionSummary
{

public static void main(String[] args) throws IOException
{
Map<String, Map<String, TransactionData>> applids =
new HashMap<String, Map<String, TransactionData>>();

ByteStringField transaction = ByteStringField.define("DFHTASK", "C001");

int noDictionary = 0;

try (SmfRecordReader reader =
args.length == 0 ?
SmfRecordReader.fromDD("INPUT") :
SmfRecordReader.fromStream(new FileInputStream(args[0])))
{
reader.include(110, Smf110Record.SMFMNSTY);
for (SmfRecord record : reader)
{
Smf110Record r110 = new Smf110Record(record);

if (r110.haveDictionary())
{
Map<String, TransactionData> applidTransactions =
applids.computeIfAbsent(
r110.mnProductSection().smfmnprn(),
transactions -> new HashMap<String, TransactionData>());

for (PerformanceRecord mn : r110.performanceRecords())
{
String txName = mn.getField(transaction);
applidTransactions.computeIfAbsent(
txName,
x -> new TransactionData(txName)).add(mn);
}
} else
{
noDictionary++;
}
}
}

writeReport(applids);

if (noDictionary > 0)
{
System.out.format(
"%n%nSkipped %s records because no applicable dictionary was found.",
noDictionary);
}

}

private static void writeReport(Map<String, Map<String, TransactionData>> transactions)
{
transactions.entrySet().stream()
.sorted((a, b) -> a.getKey().compareTo(b.getKey()))
.forEachOrdered(applid ->
{
// Headings
System.out.format("%n%-8s", applid.getKey());

System.out.format("%n%-4s %15s %15s %15s %15s %15s %15s %15s %15s %15s%n%n",
"Name",
"Count",
"Elapsed",
"Avg Elapsed",
"CPU",
"Avg CPU",
"Dispatch",
"Avg Disp.",
"Disp Wait", ""
+ "Avg Disp Wait");

applid.getValue().entrySet().stream()
.map(x -> x.getValue())
.sorted(comparing(TransactionData::getCpu, reverseOrder())
.thenComparing(TransactionData::getCount, reverseOrder()))
.forEachOrdered(txInfo ->
{
// write detail line
System.out.format("%-4s %15d %15f %15f %15f %15f %15f %15f %15f %15f%n",
txInfo.getName(),
txInfo.getCount(),
txInfo.getElapsed(),
txInfo.getAvgElapsed(),
txInfo.getCpu(),
txInfo.getAvgCpu(),
txInfo.getDispatch(),
txInfo.getAvgDispatch(),
txInfo.getDispatchWait(),
txInfo.getAvgDispatchWait());

});
});

}

private static class TransactionData
{
public TransactionData(String name)
{
this.name = name;
}

public void add(PerformanceRecord perfdata)
{
count++;
elapsed += Utils.ToSeconds(
Duration.between(perfdata.getField(start), perfdata.getField(stop)));
dispatch += perfdata.getFieldTimerSeconds(dispatchField);
dispatchWait += perfdata.getFieldTimerSeconds(dispatchWaitField);
cpu += perfdata.getFieldTimerSeconds(cpuField);
}

public String getName()
{
return name;
}

public int getCount()
{
return count;
}

public double getElapsed()
{
return elapsed;
}

public double getDispatch()
{
return dispatch;
}

public double getDispatchWait()
{
return dispatchWait;
}

public double getCpu()
{
return cpu;
}

public Double getAvgElapsed()
{
return count != 0 ? elapsed / count : null;
}

public Double getAvgDispatch()
{
return count != 0 ? dispatch / count : null;
}

public Double getAvgDispatchWait()
{
return count != 0 ? dispatchWait / count : null;
}

public Double getAvgCpu()
{
return count != 0 ? cpu / count : null;
}

static TimestampField start = TimestampField.define("DFHCICS", "T005");
static TimestampField stop = TimestampField.define("DFHCICS", "T006");
static ClockField dispatchField = ClockField.define("DFHTASK", "S007");
static ClockField dispatchWaitField = ClockField.define("DFHTASK", "S102");
static ClockField cpuField = ClockField.define("DFHTASK", "S008");

private String name;
private int count = 0;
private double elapsed = 0;
private double dispatch = 0;
private double dispatchWait = 0;
private double cpu = 0;
}
}

Sending Email from z/OS using Java

How do you send email from your z/OS system?

Java on z/OS makes this very easy. Java on z/OS can use Java libraries developed for other platforms which means there are powerful, free libraries available and plenty of documentation available via Google.

Instead of setting up a SMTP server on z/OS, you can define a user (or more than one) for z/OS in your corporate mail system, and create batch jobs that log in and send mail just like any other email user.

This example uses the JavaMail package (part of Java EE and available for download for Java SE) to send email from z/OS. It uses the JZOS Batch Launcher so you can define the input using JCL DD statements.

You can send an email using JCL like this:

//JAVAG EXEC PROC=JAVAG,
// JAVACLS='''ZMail''',
// CLASPATH='''java/lib/javax.mail.jar'''
//TO DD *
someone@example.com
//SUBJECT DD *
Test Message from batch job
//MESSAGE DD *
Message line 1
Message line 2
...
Message Line n
//

The JCL uses the JAVAG JCL PROC described in this post: Systems Programmer Friendly Java

The following code is the complete Java program. The sample uses Gmail as an example to show how to use TLS and SMTP authentication. Customize the Properties used to create the Session to suit your own mail server.

To, Subject and the Message Body are read from DD statements defined in the JCL. You could use the same pattern to allow other information to be input from DD statements, e.g. CC, BCC, From, or even the properties used to log in to the mail server.

import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;

import com.ibm.jzos.ZFile;
import com.ibm.jzos.ZFileException;

public class ZMail
{
public static void main(String[] args)
throws IOException, MessagingException
{
List<String> messagelines = readLinesFromDD("MESSAGE");
String subject = readLinesFromDD("SUBJECT").get(0);
List<String> recipients = readLinesFromDD("TO");

StringBuilder messagetext = new StringBuilder();
for (String line : messagelines)
{
messagetext.append(line);
messagetext.append(System.lineSeparator());
}

final String username = "smtp-username";
final String password = "smtp-password";

Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.port", "587");
props.put("mail.ssl.checkserveridentity", "true");

Session session = Session.getInstance(props,
new javax.mail.Authenticator()
{
protected PasswordAuthentication getPasswordAuthentication()
{
return new PasswordAuthentication(username, password);
}
});

Message message = new MimeMessage(session);
message.setFrom(new InternetAddress("me@example.com"));
for (String recipient : recipients)
{
message.addRecipients(Message.RecipientType.TO,
InternetAddress.parse(recipient));

}
message.setSubject(subject);
message.setText(messagetext.toString());

Transport.send(message);
}

private static List<String> readLinesFromDD(String dd)
throws ZFileException, IOException
{
List<String> lines = new ArrayList<String>();
ZFile input = null;
try
{
input = new ZFile("//DD:" + dd, "rt");
BufferedReader reader =
new BufferedReader(
new InputStreamReader(input.getInputStream()));

String line;
while ((line = reader.readLine()) != null)
{
lines.add(line);
}

}
finally
{
if (input != null)
{
input.close();
}
}
return lines;
}
}

Systems Programmer Friendly Java

This article describes how to set up a simple environment for running Java batch reports or utilities, including JCL PROCs similar to the compile and go PROCs that come with other languages.

Java on z/OS is great. Fast and powerful, you can perform complex functions in very few lines of code. In the past, I have used Rexx for small utilities, but now Java is my first choice.

Java is an advanced language with many features that simplify programming problems. For example, the Collections framework provides various List, Map, Set etc. classes that allow you to quickly and efficiently create, search and compare groups of data items.

The JZOS Toolkit comes with Java on z/OS and provides interfaces for working with regular z/OS datasets.

Java should be in every systems programmer’s toolbox.

The problem is there is not much guidance on how to create a simple Java batch environment on z/OS. Most of what is out there is written for large Java applications rather than simple batch jobs and as a result is unnecessarily complex.

The Objective

To create a set of procs for batch Java Compile, Go, and Compile and Go (there is no Link step for Java).

Compiling and running a Java class should be as simple as:

//ANDREWRG JOB CLASS=A,
// MSGCLASS=H,
// NOTIFY=&SYSUID
//JAVACG EXEC JAVACG,
// JAVACLS='''MyClass'''
//G.INPUT DD DISP=SHR,DSN=...

Optionally, you need to be able to override the locations for Java source, Java class files, class path etc. and the various Java options that can be specified.

The compile and link steps should be consistent so that you can switch between JAVAC, JAVAG and JAVACG without making other changes to the JCL.

Prerequisites

First, obviously, if Java is not already installed you need to install it. Refer to IBM 31-bit SDK for z/OS, Java Technology Edition, Version 8 and/or IBM 64-bit SDK for z/OS, Java Technology Edition, Version 8 for Java installation instructions.

JZOS Batch Launcher

You also need to install the JZOS Batch Launcher. This provides the functions that make Java useful in a z/OS batch environment.

Installation is straightforward. There is one load module to copy to a PDS/E, a JCL procedure and a sample job. The Readme suggests copying the load module to SYS1.SIEALNKE, however this is a SMP/E maintained dataset so if you are installing outside of SMP/E you should use a different location – create a new PDS/E if necessary.

JZOS documentation can be found here:

z/OS 2.1

These PROCs use symbol substitution in instream data which was introduced with z/OS 2.1, so z/OS 2.1 is a prerequisite. The JCL procedure delivered with JZOS works with earlier versions, but in that case the JCL required in the calling job is more complicated.

JCL Procedures

Most languages on z/OS come with a set of compile, link and go JCL procedures. These allow you to compile and run programs using standard options without dealing with the gory details every time.

We can create the same thing for Java, which greatly simplifies the process of compiling and running Java batch programs. I will be calling the PROCs JAVAC, JAVAG and JAVACG.

Design Considerations

Default Locations

Java source and class files need to be in the unix filesystem.

A standard directory structure will mean that we can set a default Java CLASSPATH etc. in the PROC, to simplify the user’s JCL.

We will use:

  • ~/java/src for the Java source
  • ~/java/target for the output class files

(~ is shorthand for the user’s home directory)

These can be overridden by the SRCPATH and TGTPATH parameters of the PROCs.

Line length

Instream data in the proc is limited to 80 bytes. If the symbol substitution extends the line beyond 80 bytes, the step gets a S001 abend. This is most likely if multiple symbols are used on one line.

To get around the problem, where we need multiple substitutions on 1 line we can assign the values to shell variables, and then use the shell variables. They are substituted later by the Unix shell which doesn’t have the same restrictions.

Java Classpath

The Java classpath is potentially lengthy with multiple entries. To allow for longer classpaths multiple CLASPAT* parameters are provided on the PROC.

To make life more complicated, the classpath syntax for the JZOS Batch Launcher is different from the Java compiler classpath syntax. The Java compiler allows wildcards but JZOS does not. We want to use the same classpath parameters in the JCL for the compile and go steps, so we need to expand wildcards in the JZOS script. To make sure the compile and go steps use the same classpath, we do the same expansion for the compile step.

Why triple quotes?

The triple quotes (apostrophes) are a quirk of JCL, which seems to be required for lowercase parameters. The enclosing quotes are stripped off when the parameters are set. The result still needs to be quoted otherwise the lowercase data causes a JCL error.

JAVAC

JAVAC is the Java compile PROC. It invokes BPXBATCH to run the Java compiler.

//JAVAC PROC JAVACLS=,
// REGSIZE='0M',
//* Parameters for instream data need triple quotes
//* to preserve quotes in the SET statement
// SRCPATH='''java/src''',
// TGTPATH='''java/target''',
// CLASPATH='''''',
//* Additional CLASPAT* entries allow CLASSPATH to
//* be extended with less JCL continuation issues.
// CLASPAT2='''''',
// CLASPAT3='''''',
// CLASPAT4='''''',
// CLASPAT5='''''',
//* Options to the java compiler
// JAVACOPT=''''''
//*
//SYMBOLS EXPORT SYMLIST=(JAVACOPT,
// SRCPATH,
// TGTPATH,
// CLASPATH,
// CLASPAT2,
// CLASPAT3,
// CLASPAT4,
// CLASPAT5,
// JAVACLS)
//*
// SET JAVACLS=&JAVACLS
// SET SRCPATH=&SRCPATH
// SET TGTPATH=&TGTPATH
// SET CLASPATH=&CLASPATH
// SET CLASPAT2=&CLASPAT2
// SET CLASPAT3=&CLASPAT3
// SET CLASPAT4=&CLASPAT4
// SET CLASPAT5=&CLASPAT5
// SET JAVACOPT=&JAVACOPT
//*
//C EXEC PGM=BPXBATCH,REGION=&REGSIZE
//* STDPARM is multiple commands chained into one with
//* semicolons as an argument to "SH"
//* IFS splits CLASSPATH based on ":"
//* Then wildcards are expanded and recombined
//STDPARM DD *,SYMBOLS=JCLONLY
SH CLASSPATH=&TGTPATH;
IFS=":";
for i in &CLASPATH; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
for i in &CLASPAT2; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
for i in &CLASPAT3; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
for i in &CLASPAT4; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
for i in &CLASPAT5; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
export CLASSPATH="${CLASSPATH}";
SRCPATH=&SRCPATH;
TGTPATH=&TGTPATH;
JAVACLS=&JAVACLS;
/usr/lpp/java/J8.0/bin/javac
&JAVACOPT
-sourcepath
${SRCPATH}
-d
${TGTPATH}
${SRCPATH}/${JAVACLS}.java
//STDENV DD DDNAME=ENV
// DD DDNAME=ADDENV
//ADDENV DD DISP=SHR,DSN=NULLFILE
//ENV DD *
HJV_JZOS_JVM_SMF_LOGGING=true
HJV_JZOS_JVM_SMF_LOGGING_INTERVAL=10
HJV_JZOS_JVM_SMF_THREADS=true
//STDOUT DD SYSOUT=*
//STDERR DD SYSOUT=*
//*
// PEND

JAVAC Examples

//ANDREWRG JOB CLASS=A,
// MSGCLASS=H,
// NOTIFY=&SYSUID
//JAVACG EXEC PROC=JAVAC,
// JAVACLS='''helloworld'''
//ANDREWRC JOB CLASS=A,
// MSGCLASS=H,
// NOTIFY=&SYSUID
//*
//JAVAC EXEC JAVAC,
// JAVACLS='''com/blackhillsoftware/samples/RecordCount''',
// SRCPATH='''java/easysmf-je-1-5-2/samples/source''',
// JAVACOPT='''-Xlint -verbose''',
// CLASPATH='''java/easysmf-je-1-5-2/jar/*'''
STDENV, ENV and ADDENV DD Statements

The STDENV DD statement allows you to specify enviroment variables for the BPXBATCH step.

STDENV concatenates 2 other DD statements:

  • ENV has default environment variables you want to set for all users of the proc.
  • ADDENV is a null file which can be overridden by the calling job to add or modify the default environment
//C.ADDENV DD *
MYVARIABLE=ABCD
CLASSPATH

CLASSPATH can be set using wildcards or explicit entries. You can have multiple entries on one line separated by “:”, e.g.

// CLASPATH='''java/easysmf-je-1-5-2/jar/*''',
// CLASPAT2='''java/lib/javax.mail.jar:java/lib/jsoup-1.10.2.jar'''

The shell script sets the field separator (IFS) to “:” so it can split multiple entries in the class path and expand any wildcards, before recombining all the entries.

JAVAG

JAVAG is based on the JVMPRC80 and JVMJCL80 samples delivered with the JZOS Batch Launcher.

The embedded script has been modified to set the classpath using the same syntax as the JAVAC proc.

//JAVAG PROC JAVACLS=,
// ARGS=,
// LIBRARY='PDSE.CONTAINING.JVMLDM80',
// LOGLVL='',
// REGSIZE='0M',
// LEPARM='',
//* Parameters for instream data need triple quotes
//* to preserve quotes in the SET statement
// SRCPATH='''java/src''',
// TGTPATH='''java/target''',
// CLASPATH='''''',
//* Additional CLASPAT* entries allow CLASSPATH to
//* be extended with less JCL continuation issues.
// CLASPAT2='''''',
// CLASPAT3='''''',
// CLASPAT4='''''',
// CLASPAT5=''''''
//*
//SYMBOLS EXPORT SYMLIST=(JAVACOPT,
// SRCPATH,
// TGTPATH,
// CLASPATH,
// CLASPAT2,
// CLASPAT3,
// CLASPAT4,
// CLASPAT5,
// JAVACLS)
//*
// SET JAVACLS=&JAVACLS
// SET SRCPATH=&SRCPATH
// SET TGTPATH=&TGTPATH
// SET CLASPATH=&CLASPATH
// SET CLASPAT2=&CLASPAT2
// SET CLASPAT3=&CLASPAT3
// SET CLASPAT4=&CLASPAT4
// SET CLASPAT5=&CLASPAT5
//*
//G EXEC PGM=JVMLDM80,REGION=&REGSIZE,
// PARM='&LEPARM/&LOGLVL &JAVACLS &ARGS'
//*
//STEPLIB DD DSN=&LIBRARY,DISP=SHR
//SYSPRINT DD SYSOUT=*
//SYSOUT DD SYSOUT=*
//STDOUT DD SYSOUT=*
//STDERR DD SYSOUT=*
//CEEDUMP DD SYSOUT=*
//ABNLIGNR DD DUMMY
//STDENV DD DDNAME=ENV
// DD DDNAME=ADDENV
//ADDENV DD DISP=SHR,DSN=NULLFILE
//ENV DD *,SYMBOLS=JCLONLY
. /etc/profile
export JAVA_HOME=/usr/lpp/java/J8.0
export PATH=/bin:"${JAVA_HOME}"/bin

LIBPATH=/lib:/usr/lib:"${JAVA_HOME}"/bin
LIBPATH="$LIBPATH":"${JAVA_HOME}"/lib/s390
LIBPATH="$LIBPATH":"${JAVA_HOME}"/lib/s390/j9vm
LIBPATH="$LIBPATH":"${JAVA_HOME}"/bin/classic
export LIBPATH="$LIBPATH":

APP_HOME=&TGTPATH
CLASSPATH="${APP_HOME}"
IFS=':'
for i in &CLASPATH; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
for i in &CLASPAT2; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
for i in &CLASPAT3; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
for i in &CLASPAT4; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
for i in &CLASPAT5; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
export CLASSPATH="${CLASSPATH}"

IJO="-Xms16m -Xmx128m"
export IBM_JAVA_OPTIONS="$IJO "

export HJV_JZOS_JVM_SMF_LOGGING=true
export HJV_JZOS_JVM_SMF_LOGGING_INTERVAL=10
export HJV_JZOS_JVM_SMF_THREADS=true
// PEND

JAVAG Examples

//JAVAG EXEC PROC=JAVAG,
// JAVACLS='''helloworld'''
//*JAVACLS='''com/blackhillsoftware/ivp'''
//*JAVACLS='''-jar /u/ajr/java/helloworld.jar'''
//G.INPUT DD DISP=SHR,DSN=ANDREWR.SMF.SAMPLE
//G.ADDENV DD *
IJO="$IJO -verbose:class"
export IBM_JAVA_OPTIONS="$IJO "

This sample JCL shows 3 different types of Java class that can be invoked:

  1. A class which is not part of a package.
  2. A class (ivp) which is a member of package com.blackhillsoftware.
  3. A runnable jar.

//G.INPUT is an example of a DDNAME used by a particular Java program, not a DD required by the PROC.

STDENV, ENV and ADDENV DD Statements

The STDENV DD statement provides a script that is run before the Java program is invoked. It is used to set environment variables.

Unfortunately, the syntax is different to STDENV for BPXBATCH in the compile step. BPXBATCH STDENV sets the environment variables directly, while JZOS requires a script. The JZOS approach adds some flexibility, but means you need to explicitly “export” the variables.

As with the compile step, STDENV concatenates 2 other DD statements:

  • ENV has the standard script inline in the PROC.
  • ADDENV is a null file which can be overridden by the calling job to add or modify the default environment.
//G.ADDENV DD *
IJO="$IJO -verbose:class"
export IBM_JAVA_OPTIONS="$IJO "
CLASSPATH

CLASSPATH is set using the same syntax as the compile PROC.

JAVACG

JAVACG combines the C and G PROCs, with a condition code check after the BPXBATCH compile step.

//JAVACG PROC JAVACLS=,
// ARGS=,
// LIBRARY='PDSE.CONTAINING.JVMLDM80',
// LOGLVL='',
// REGSIZE='0M',
// LEPARM='',
//* Parameters for instream data need triple quotes
//* to preserve quotes in the SET statement
// SRCPATH='''java/src''',
// TGTPATH='''java/target''',
// CLASPATH='''''',
//* Additional CLASPAT* entries allow CLASSPATH to
//* be extended with less JCL continuation issues.
// CLASPAT2='''''',
// CLASPAT3='''''',
// CLASPAT4='''''',
// CLASPAT5='''''',
//* Options to the java compiler
// JAVACOPT=''''''
//*
//SYMBOLS EXPORT SYMLIST=(JAVACOPT,
// SRCPATH,
// TGTPATH,
// CLASPATH,
// CLASPAT2,
// CLASPAT3,
// CLASPAT4,
// CLASPAT5,
// JAVACLS)
//*
// SET JAVACLS=&JAVACLS
// SET SRCPATH=&SRCPATH
// SET TGTPATH=&TGTPATH
// SET CLASPATH=&CLASPATH
// SET CLASPAT2=&CLASPAT2
// SET CLASPAT3=&CLASPAT3
// SET CLASPAT4=&CLASPAT4
// SET CLASPAT5=&CLASPAT5
// SET JAVACOPT=&JAVACOPT
//*
//C EXEC PGM=BPXBATCH,REGION=&REGSIZE
//* STDPARM is multiple commands chained into one with
//* semicolons as an argument to "SH"
//* IFS splits CLASSPATH based on ":"
//* Then wildcards are expanded and recombined
//STDPARM DD *,SYMBOLS=JCLONLY
SH CLASSPATH=&TGTPATH;
IFS=":";
for i in &CLASPATH; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
for i in &CLASPAT2; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
for i in &CLASPAT3; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
for i in &CLASPAT4; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
for i in &CLASPAT5; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${i}";
done;
done;
export CLASSPATH="${CLASSPATH}";
SRCPATH=&SRCPATH;
TGTPATH=&TGTPATH;
JAVACLS=&JAVACLS;
/usr/lpp/java/J8.0/bin/javac
&JAVACOPT
-sourcepath
${SRCPATH}
-d
${TGTPATH}
${SRCPATH}/${JAVACLS}.java
//STDENV DD DDNAME=ENV
// DD DDNAME=ADDENV
//ADDENV DD DISP=SHR,DSN=NULLFILE
//ENV DD *
HJV_JZOS_JVM_SMF_LOGGING=true
HJV_JZOS_JVM_SMF_LOGGING_INTERVAL=10
HJV_JZOS_JVM_SMF_THREADS=true
//STDOUT DD SYSOUT=*
//STDERR DD SYSOUT=*
//*
//G EXEC PGM=JVMLDM80,REGION=&REGSIZE,
// PARM='&LEPARM/&LOGLVL &JAVACLS &ARGS',
// COND=(0,NE,C)
//*
//STEPLIB DD DSN=&LIBRARY,DISP=SHR
//SYSPRINT DD SYSOUT=*
//SYSOUT DD SYSOUT=*
//STDOUT DD SYSOUT=*
//STDERR DD SYSOUT=*
//CEEDUMP DD SYSOUT=*
//ABNLIGNR DD DUMMY
//STDENV DD DDNAME=ENV
// DD DDNAME=ADDENV
//ADDENV DD DISP=SHR,DSN=NULLFILE
//ENV DD *,SYMBOLS=JCLONLY
. /etc/profile
export JAVA_HOME=/usr/lpp/java/J8.0
export PATH=/bin:"${JAVA_HOME}"/bin

LIBPATH=/lib:/usr/lib:"${JAVA_HOME}"/bin
LIBPATH="$LIBPATH":"${JAVA_HOME}"/lib/s390
LIBPATH="$LIBPATH":"${JAVA_HOME}"/lib/s390/j9vm
LIBPATH="$LIBPATH":"${JAVA_HOME}"/bin/classic
export LIBPATH="$LIBPATH":

APP_HOME=&TGTPATH
CLASSPATH="${APP_HOME}"
IFS=':'
for i in &CLASPATH; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
for i in &CLASPAT2; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
for i in &CLASPAT3; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
for i in &CLASPAT4; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
for i in &CLASPAT5; do
for j in ${i}; do
CLASSPATH="${CLASSPATH}":"${j}"
done
done
export CLASSPATH="${CLASSPATH}"

IJO="-Xms16m -Xmx128m"
export IBM_JAVA_OPTIONS="$IJO "

export HJV_JZOS_JVM_SMF_LOGGING=true
export HJV_JZOS_JVM_SMF_LOGGING_INTERVAL=10
export HJV_JZOS_JVM_SMF_THREADS=true
// PEND

JCL to invoke it looks like:

//JAVAC EXEC PROC=JAVA8UCG,
// JAVACLS='smf/joblist'
//*
//* DD statement for the Java program
//G.INPUT DD DISP=SHR,DSN=SMF.DATA
//* Optional DD statements to demonstrate
//* modifing compile/go environments
//G.ADDENV DD *
IJO="$IJO -verbose:class"
export IBM_JAVA_OPTIONS="$IJO "

Writing your Java program

Most examples suggest using an IDE like Eclipse to write and compile your program, transferring the class files to z/OS to run.

Eclipse is a very productive environment for writing Java and if you are used to using Eclipse, writing Java without it is painful. However, writing Java is no different to writing in any other language on z/OS and the simplicity of working with a single platform and familiar tools has a lot to recommend it.

I suggest getting a simple batch environment working on z/OS, and only then expanding the environment to use Eclipse, build and deploy scripts etc. if required. It is much easier to setup and debug more complex functions if you start with a simple, working batch environment.

Java programs must live in Unix directories due to the way Java uses the directory structure. ISPF option 3.17 provides a nice environment for editing z/OS unix files.

> Tip: ISPF highlighting doesn’t have a Java option, but setting the language to C works pretty well for matching braces, showing comments etc.

Those pesky class not found errors

First efforts to run Java on z/OS often result in mystifying class not found errors. You look at the CLASSPATH, you can see the Java class file there, why can’t Java find it?

The answer lies in how Java classes are organized into packages. Packages avoid naming conflicts between classes, but Java requires that the classes are located in subdirectories that reflect the package name.

z/OS (and Windows/Unix for that matter), searches the exact locations specified (LINKLIST, STEPLIB, path etc) for executable programs. Java searches for classes in locations relative to the CLASSPATH directory, based on the class package name.

If we have a simple Java program:

public class helloworld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

This program doesn’t specify a package, so the source and class files must be in the directories specified in the SOURCEPATH and CLASSPATH.

However, if we decide to create a “helloworld” package and a “testhello” package (perhaps to share the function with other programs):

package testhello;
// This class doesn't do anything
// until called from another class
public class hello {
public static void sayHello() {
System.out.println("Hello World!");
}
}

helloworld becomes:

package helloworld;
import testhello.hello;
// This class has a main(...) method
// which java calls to invoke the program
public class helloworld {
public static void main(String[] args) {
hello.sayHello();
}
}

The helloworld class is in the helloworld package, so the PROCs specify:

// JAVACLS='helloworld/helloworld'

If the runtime CLASSPATH is set to
/u/andrew/java Java will expect to find /u/andrew/java/helloworld/helloworld.class
and /u/andrew/java/testhello/hello.class.

If your CLASSPATH is set to /u/andrew/java and the hello.class and helloworld.class files are located in that directory, Java will say they cannot be found… which can be puzzling to say the least. The answer is that the location relative to the CLASSPATH must match the package name.

Java source files are also required to use the same hierarchy. A class in package mypackage MUST be in the mypackage/ subdirectory or you will get a compilation error.

> Actually, that’s not completely true… if you point the compiler at a Java file that specifies a package, that class will compile OK. It will also find other classes in the same package if they are in the same directory. However, it will locate other classes by going up the directory tree to what it considers the package root, and then looking for subdirectories according to package names.
> The class files produced will be placed in subdirectories of the output directory.

This means that Java source code on z/OS must be in HFS directories – unfortunately, you can’t use regular z/OS datasets for your source code.

Jar files follow the same principle, except that the directory hierarchy is embedded in the jar file itself. You can see this if you extract a jar file with a zip program.

Naming packages

To avoid name clashes there is a naming convention for Java packages. According to convention, the package name begins with your internet domain name reversed, i.e. Black Hill Software uses com.blackhillsoftware. Subsequent components are based on your own naming conventions.

You might choose not to use the package naming conventions for your own programs, however if usage becomes more formalized e.g. you develop a library of commonly used functions it is a good idea to use names that adhere to the standard.

Either way, packages provide a good way to organize your programs inside the ~/java/src etc. directory structure. You can separate programs into packages, which allows you to organize the programs into subdirectories based on package name.

Conclusion

A set of JCL procedures for compiling and running Java programs makes Java much more useful on z/OS.

Once you can easily compile and run Java on z/OS it becomes an excellent language for writing small batch utilities and productivity tools.

What Next?

Why not try using Java for SMF reporting : EasySMF:JE Java Quickstart

z/OS Productivity Tools using Java

This article examines how Java can be a powerful productivity tool for a Systems Programmer, using the task of synchronizing Master Catalogs as an example.

Systems programmers are often dismissive of Java. They think it is memory hungry, has poor performance and is not useful for real systems programming work.

In fact, the reality is that Java performance on z/OS is good, and being improved all the time.

More importantly, it is a powerful language and when combined with the functions provided by the JZOS Toolkit can be very useful for writing System Programmer utilities.

Example: Synchronizing Master Catalogs

Comparing or synchronizing master catalogs is a common task. You may have several systems with separate master catalogs, or need to merge a master catalog from a Serverpac installation with a live master catalog.

This Java example generates IDCAMS commands to synchronize catalogs. It could be adapted to define new entries only, work with specific HLQs etc.

The power of Java Collections

Java includes a Collections framework. As its name implies, the Collections framework provides functions for working with collections of data – Lists, Maps, Sets etc.

One of the most useful Collections classes is the Map, which maps keys to values. I usually use the HashMap specialization, which uses a hash table to map the values and generally has excellent performance.

Using a Map, the logic for comparing catalogs is simple:

  1. Add entries from the first catalog to a map using the name as a key
  2. Get all entries from the second catalog and:
    1. If there is a matching name in the map – remove the entry from the map and compare the attributes.
    2. Otherwise, record that the entry is in the second catalog only.
  3. The remaining entries in the map are in the first catalog only.

Sample code showing the logic:

Map<String, CatalogEntry> cat1Entries
= new HashMap<String, CatalogEntry>();
for (CatalogEntry entry :
getCatalogEntries(catalog1, "**", null))
{
cat1Entries.put(entry.entryName, entry);
}

List<CatalogEntry> cat2Only
= new ArrayList<CatalogEntry>();

for (CatalogEntry cat2Entry :
getCatalogEntries(catalog2, "**", null))
{
CatalogEntry cat1Entry
= cat1Entries.remove(cat2Entry.entryName);
if (cat1Entry != null) // found entry from catalog1
{
if (!cat2Entry.equals(cat1Entry))
{
// same name, different attributes
}
// else entries match
}
else
{
// entry is in catalog 2 only
}
}
// Remaining entries in map are in catalog 1 only
List<CatalogEntry> cat1Only
= new ArrayList<CatalogEntry>(
cat1Entries.values());

Catalog comparison logic

CatalogSync Implementation

The program consists of a CatalogEntry class, which provides the functions to work with catalog entries, and the main program with the comparison logic.

CatalogEntry Class

CatalogEntry instances are created from entries returned from the JZOS CatalogSearch function. The CatalogEntry constructor takes the JZOS CatalogSearch.Entry and extracts the information we are interested in.

import java.util.*;
import com.ibm.jzos.CatalogSearch;

public class CatalogEntry {
public CatalogEntry(CatalogSearch.Entry entry)
{
entryName =
entry.getField("ENTNAME").getFString();
entryType =
entry.getField("ENTYPE").getFString();
relatedNames =
entry.getField("NAME").getFStringArray(44);
if (relatedNames == null)
relatedNames = new String[] {};
volumes =
entry.getField("VOLSER").getFStringArray(6);
if (volumes == null)
volumes = new String[] {};
deviceTypes =
entry.getField("DEVTYP").getIntArray(4);
if (deviceTypes == null)
deviceTypes = new int[] {};
}

public String entryName;
public String entryType;
public String[] relatedNames;
public String[] volumes;
public int[] deviceTypes;
}

CatalogEntry.java class with constructor

The NAME, VOLSER and DEVTYP from CatalogSearch.Entry can be null if the attribute does not exist for that entry. In that case they are set to empty arrays, because that simplifies comparison processing.

Our CatalogEntry class has the following additional methods:

CatalogEntry.getCatalogEntries

getCatalogEntries reads entries from the specified catalog.

It is a static method which is used by the main program to retrieve the list of entries.

getCatalogEntries uses the JZOS CatalogSearch class which provides a Java interface to the z/OS Catalog Search Interface (CSI).

public static List<CatalogEntry> getCatalogEntries(
String catalogname,
String search,
String entryTypes)
{
List<CatalogEntry> result =
new ArrayList<CatalogEntry>();
CatalogSearch catSearch =
new CatalogSearch(search);
catSearch.setCatalogName(catalogname);
catSearch.setEntryTypes(entryTypes);
catSearch.setCatalogName(catalogname);
catSearch.setSingleCatalog(true);
catSearch.addFieldName("ENTYPE");
catSearch.addFieldName("ENTNAME");
catSearch.addFieldName("NAME");
catSearch.addFieldName("DEVTYP");
catSearch.addFieldName("VOLSER");
catSearch.search();
while (catSearch.hasNext())
{
CatalogSearch.Entry entry =
(CatalogSearch.Entry)catSearch.next();
if (entry.isDatasetEntry())
{
result.add(new CatalogEntry(entry));
}
}
return result;
}

CatalogEntry.getCatalogEntries method

getCatalogEntries sets various CSI parameters then invokes the search, returning the result as a List.

Note: The line catSearch.setSingleCatalog(true) says that only the specified catalog will be searched. Otherwise entries from multiple catalogs (e.g. the current master catalog) can be returned. This gives false results when trying to compare 2 catalogs.

For more information on the CatalogSearch function refer to the JZOS CatalogSearch documentation, and the Catalog Search Interface documentation in the z/OS Managing Catalogs manual.

CatalogEntry.equals and CatalogEntry.hashcode

CatalogEntry.equals tests whether 2 catalog entries are equal.

CatalogEntry.hashcode is not used in this program, but is included because when you override the equals function it is good practice to also override the hashcode function. Equals and hashcode are used when the class is used as a key in a HashMap. The rule is that if 2 instances are equal they must have the same hashcode.

This class simply returns the hashcode from the entry name as the CatalogEntry.hashcode.1

@Override
public boolean equals(Object o)
{
if (o == this) return true; // same object
if (!(o instanceof CatalogEntry)) return false;
CatalogEntry ce = (CatalogEntry) o;
return (Objects.equals(
this.entryName, ce.entryName)
&& Objects.equals(
this.entryType, ce.entryType)
&& Arrays.equals(
this.relatedNames, ce.relatedNames)
&& Arrays.equals(
this.volumes, ce.volumes)
&& Arrays.equals(
this.deviceTypes, ce.deviceTypes)
);
}

@Override
public int hashCode()
{
return entryName.hashCode();
}

CatalogEntry.equals and hashcode methods

CatalogEntry.deviceType

This is a utility function to translate the device type from the catalog to a device type for the IDCAMS command.

String deviceType(int devtype)
{
switch (devtype)
{
case 0:
return "0000";
case 0x3010200E:
return "3380";
case 0x3010200F:
return "3390";
default:
return String.format(
"Device type %8X not implemented",
devtype);
}
}

CatalogEntry. deviceType method

CatalogEntry.deleteCommand, defineCommand

The deleteCommand and defineCommand methods generate the IDCAMS commands used to synchronize the catalogs.

The methods use String formatting to insert the values into the commands.

public String deleteCommand(String catalog)
{
switch (entryType)
{
case "A": //NONVSAM
return String.format(
" DELETE -%n" +
" %s -%n" +
" NOSCRATCH -%n"+
" CATALOG(%s)%n",
entryName,
catalog);
case "X": //ALIAS
return String.format(
" DELETE -%n" +
" %s -%n" +
" ALIAS -%n"+
" CATALOG(%s)%n",
entryName,
catalog);
default:
return String.format(
"Delete entry type %s " +
"not implemented, %s%n",
entryType, entryName);
}
}

CatalogEntry.deleteCommand method

public String defineCommand(String catalog)
{
switch (entryType)
{
case "A": //NONVSAM
String volList = volumes[0];
for (int i=1; i < volumes.length; i++)
{
volList += " " + volumes[i];
}
String devList =
deviceType(deviceTypes[0]);
for (int i=1;
i < deviceTypes.length; i++)
{
devList +=
" " + deviceType(deviceTypes[i]);
}
return String.format(
" DEFINE NONVSAM -%n" +
" (NAME(%s) -%n" +
" DEVICETYPES(%s) -%n"+
" VOLUMES(%s) )-%n"+
" CATALOG(%s)%n",
entryName,
devList,
volList,
catalog);
case "X": //ALIAS
return String.format(
" DEFINE ALIAS -%n" +
" (NAME(%s) -%n" +
" RELATE(%s) ) -%n" +
" CATALOG(%s)%n",
entryName,
relatedNames[0],
catalog);
default:
return String.format(
"Define entry type %s " +
"not implemented, %s%n",
entryType, entryName);
}
}

CatalogEntry.defineCommand method

defineCommand handles the case where there are multiple volumes in the entry by building strings with the volumes and device types before inserting them into the command.

The CatalogSync program

This is the main program to generate the catalog synchronization commands.


DANGER

Automatically generated commands can severely damage your system if they contain errors. Before running the commands generated by this program, please make sure that:

  • The generated commands are correct, and operating on the correct catalogs.
  • All required continuation characters are present so that the statement specifying the target catalog is part of the statement.
  • You understand what the commands are doing.
  • You have a plan for recovery if an error occurs.

The catalog names are passed as arguments to the program, and the delete/define commands are written to the DD COMMANDS.

This program only deals with NONVSAM and ALIAS entries, so the call to getCatalogEntries specifies “AX” as the entry types. (See the CSI documentation in the IBM Managing Catalogs manual.)

import java.io.*;
import java.util.*;
import com.ibm.jzos.*;

public class CatalogSync
{
public static void main(String[] args)
throws IOException
{
if (args.length != 2)
{
System.out.println(
"Usage: CatalogSync catalog1 catalog2");
return;
}
String cat1 = args[0];
String cat2 = args[1];

try (PrintWriter writer
= new PrintWriter(
FileFactory.newOutputStream(
"//DD:COMMANDS")))
{
Map<String, CatalogEntry> cat1Entries
= new HashMap<String, CatalogEntry>();
for (CatalogEntry entry :
CatalogEntry.getCatalogEntries(
cat1, "**", "AX"))
{
cat1Entries.put(
entry.entryName, entry);
}

for (CatalogEntry cat2entry :
CatalogEntry.getCatalogEntries(
cat2, "**", "AX"))
{
CatalogEntry cat1entry = cat1Entries
.remove(cat2entry.entryName);
if (cat1entry != null)
{
if (!cat2entry.equals(cat1entry))
{
writer.print(
cat2entry.deleteCommand(
cat2));
writer.print(
cat1entry.defineCommand(
cat2));
}
// else entries match
} else
{
writer.print(
cat2entry.deleteCommand(
cat2));
}
}

List<CatalogEntry> cat1Only
= new ArrayList<CatalogEntry>(
cat1Entries.values());
Collections.sort(cat1Only,
new Comparator<CatalogEntry>()
{
public int compare(
CatalogEntry s1, CatalogEntry s2)
{
return
s1.entryName.compareTo(
s2.entryName);
}
});

for (CatalogEntry entry : cat1Only)
{
writer.print(
entry.defineCommand(cat2));
}
}
}
}

CatalogSync.java class

Compiling and Running the Program

This JCL uses the PROCs described in the article Systems Programmer Friendly Java to compile and run the complete program:

//JAVAG EXEC PROC=JAVA8UCG,
// JAVACLS='CatalogSync'
//G.MAINARGS DD *
CATALOG.MASTER
CATALOG.MASTER.COPY
//G.COMMANDS DD DISP=SHR,DSN=USERID.JCL.CNTL(GENCMDS)

Compile and Execution JCL

Conclusion

Java is a powerful language and a useful addition to z/OS. Hopefully this article has given you some ideas about how it could make your job easier.


Footnote 1

1: Hash collisions can cause performance problems when using hash tables, so it is recommended that all the members that are used to determine equality are used when calculating a hashcode. In this case I am using the lazy assumption that CatalogEntries used as keys in a hashtable will normally have different entry names.

Java SMF Sample 1 : SMF Records by type and subtype

This sample program reads all the SMF records from a file or dataset, and for each record type and subtype lists the number of records, total bytes and percentage of the total.

These examples are not intended to suggest any particular coding style or structure for Java programs. They are intended to show what is possible and illustrate how to access various features of the Java SMF API. In fact I have deliberately ignored some Java conventions in these samples (e.g. encapsulation in the inner classes) to cut the total amount of code and concentrate on illustrating the SMF processing facilities.

To start with, I’ll skip the most of the Java stuff and concentrate on the SMF processing.

Reading SMF data

The SmfRecordReader class provides the facility to read SMF records. It uses a JZOS RecordReader internally to read from a DDNAME or reads directly from a stream.

A stream can be any type of InputStream. Typically it would be a file but it could also be some sort of network stream, or you could chain streams together to read compressed data etc. The stream must contain the record descriptor words (RDWs) so the record lengths can be determined.

SmfRecordReader implements AutoCloseable which means that it can be used in a try-with-resources block to automatically close the reader when exiting the block.

To open a DD for reading:

try (SmfRecordReader reader = SmfRecordReader.fromDD("INPUT")) {
...
}

To read from a file:

try (SmfRecordReader reader = SmfRecordReader
.fromStream(new FileInputStream(
"C:\\Users\\Andrew\\Documents\\SMF Data\\weekly.smf"))) {
...
}

The samples handle both cases by accepting the filename as an argument to the program. If no filename is provided they open the INPUT DD:

try (SmfRecordReader reader =
args.length == 0 ?
SmfRecordReader.fromDD("INPUT") :
SmfRecordReader.fromStream(new FileInputStream(args[0])))
{
...
}

This makes it simple to develop and test on the workstation, then move the program to z/OS to run as a batch job.

Processing the data

for (SmfRecord record : reader)
{        
// do stuff with record...
};

SmfRecordReader implements the Iterable<SmfRecord> interface. The loop above will read and process each record until the reader indicates that no more records are available. Each iteration provides the next SmfRecord in the record variable.

Accessing information from the record

Record information can be accessed using record methods:

Integer key = (record.recordType() << 16)
+ (record.hasSubtypes() ? record.subType() : 0);
int length = record.recordLength();

That’s all the SMF processing this program does.

The Java Stuff

You could probably store record type and subtype information in a very sparse 2 dimensional array, but for reduced memory usage this program uses a HashMap with the type and subtype combined into an Integer key, and an object containing the statistics as the value.

Map<Integer, RecordStats> statsMap =
new HashMap<Integer, RecordStats>();

RecordStats is the class that keeps the counters for a type/subtype combination.

The processing is simple:

For each record:
1. Get the type/subtype key
2. Try to get an existing statistics entry for the key
3. If there is no existing entry, create a new RecordStats entry

Integer key = (record.recordType() << 16)
+ (record.hasSubtypes() ? record.subType() : 0);

RecordStats stats = statsMap.get(key);
if (stats == null)
{
statsMap.put(key, new RecordStats(record));
}
else
{
stats.add(record);
}

SMF record type is a 1 byte field and subtype is 2 bytes, so they can both be combined into a 4 byte Integer to form the key.

Output

The writeReport method produces the output.

The statistics entries are sorted by the largest to smallest number of bytes. First the map entries need to be put into a List for sorting:

List<Entry<Integer, RecordStats>> results =
new ArrayList<Entry<Integer, RecordStats>>(statsMap.entrySet());

Then the Collections.sort method is used to sort the list. We can provide a Comparator in the call to sort to sort by anything we like. In this case we compare the bytes in the RecordStats object.

Collections.sort(results,
new Comparator<Entry<Integer, RecordStats>>()
    {
     public int compare(
         Entry<Integer, RecordStats> s1,
            Entry<Integer, RecordStats> s2)
            {
             return Long.compare( // reversed to sort descending
                 s2.getValue().bytes,
                    s1.getValue().bytes);
            }
    });

Then System.out.format is used to write headings and data. The output:

Type  Subtype     Records          MB     Pct       Min       Max       Avg
   74        5       60384        1330    23.5      4668     32484     23107
   30        4      857136        1204    21.3      1137     32740      1473
   30        3      853873        1195    21.1       626     32740      1468
   74        1       37536        1128    19.9     17668     32632     31517
   30        5       97123         236     4.2      1137     32740      2558
...

The complete Java program:

import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.blackhillsoftware.smf.SmfRecord;
import com.blackhillsoftware.smf.SmfRecordReader;

public class recordcount
{
public static void main( String[] args ) throws IOException
{
// use a HashMap to keep the statistics
Map<Integer, RecordStats> statsMap =
new HashMap<Integer, RecordStats>();

// Read from a DD or file
try (SmfRecordReader reader =
args.length == 0 ?
SmfRecordReader.fromDD("INPUT") :
SmfRecordReader.fromStream(new FileInputStream(args[0])))
{

// SmfRecordReader implements Iterable<SmfRecord>, so we can
// simply iterate to read SmfRecords
for (SmfRecord record : reader)
{
Integer key = (record.recordType() << 16)
+ (record.hasSubtypes() ? record.subType() : 0);

// try to retrieve an existing entry
RecordStats stats = statsMap.get(key);
if (stats == null) // none -> create new
{
statsMap.put(key, new RecordStats(record));
}
else // otherwise add this record to existing
{
stats.add(record);
}
};
}
writeReport(statsMap);
}

private static void writeReport(Map<Integer, RecordStats> statsMap)
{
// Turn the HashMap into a list so it can be sorted
List<Entry<Integer, RecordStats>> results =
new ArrayList<Entry<Integer, RecordStats>>(statsMap.entrySet());

// get the total bytes from all record types
long totalbytes = 0;
for (Entry<Integer, RecordStats> entry : results)
{
totalbytes += entry.getValue().bytes;
}

// sort by total bytes descending
Collections.sort(results,
new Comparator<Entry<Integer, RecordStats>>()
{
public int compare(
Entry<Integer, RecordStats> s1,
Entry<Integer, RecordStats> s2)
{
return Long.compare( // reversed to sort descending
s2.getValue().bytes,
s1.getValue().bytes);
}
}
);

// write heading
System.out.format("%5s %8s %11s %11s %7s %9s %9s %9s%n",
"Type",
"Subtype",
"Records",
"MB",
"Pct",
"Min",
"Max",
"Avg");

// write data
for (Entry<Integer, RecordStats> entry : results)
{
System.out.format("%5d %8d %11d %11d %7.1f %9d% 9d% 9d%n",
entry.getKey().intValue() >> 16,
entry.getKey().intValue() & 0xFFFF,
entry.getValue().count,
entry.getValue().bytes / (1024*1024),
(float)(entry.getValue().bytes) / totalbytes * 100,
entry.getValue().min,
entry.getValue().max,
entry.getValue().bytes / entry.getValue().count);
}
}

/**
* Statistics for a type/subtype combination
*/
private static class RecordStats
{
public RecordStats(SmfRecord record)
{
count = 1;
bytes = min = max = record.recordLength();
}

int count;
long bytes;
int max;
int min;

public void add(SmfRecord record)
{
count++;
int length = record.recordLength();
bytes += length;
if (length < min)
min = length;
if (length > max)
max = length;
}
}
}

Java SMF Sample 2 : Prime Shift Top CPU Consumers

This second sample illustrates the use of java.time functions to report based on time and the day of the week. It reports the top 10 CPU consuming jobs by jobname between 8:30 am and 5:30 pm for each weekday.

Opening the file and reading the records work the same way as the previous sample. However in this case the processing is slightly more complicated.

Program Setup

We use a HashMap<String, JobData> to map job names to the cumulative job information. We want to report the days of the week separately, so we create a List containing a Map for each day of the week. The days of the week are numbered 1-7. We create a list of 8 entries so we can use indexes 1-7, leaving entry 0 unused.

List<HashMap<String, JobData>> jobsByDay =
        new ArrayList<HashMap<String, JobData>>();
        
for (int i=0; i <= 7; i++)
{
     jobsByDay.add(new HashMap<String, JobData>());
}

Java Time LocalTime variables are used to define the times of day we are interested in:

LocalTime primeStart = new LocalTime(8, 30);
LocalTime primeEnd = new LocalTime(17, 30);

Processing the records

Firstly we check whether the record is one we are interested in: type 30 subtype 2 and 3, written during the prime shift we defined.

if (record.recordType() == 30
&& (record.subType() == 2 || record.subType() == 3))
{               
DayOfWeek recordDayOfWeek = record.smfDate().getDayOfWeek();
    LocalTime recordTime = record.smfTime();
                
    if (recordDayOfWeek.getValue() >= DayOfWeek.MONDAY.getValue()
     && recordDayOfWeek.getValue() <= DayOfWeek.FRIDAY.getValue()
        && recordTime.isAfter(primeStart)
        && recordTime.isBefore(primeEnd))
    {
     // process this record...
    }
}

For each record that matches we need to:

  1. Create a Smf30Record from the SmfRecord, to give access to the type 30 information.
  2. Get the collection of jobs for this day of the week.
  3. Extract the job name from the Identification Section, and look for existing statistics for the job name.
  4. Update or add job statistics with information from the Processor Accounting Section.

Each type 30 record has an Identification Section, so the r30.identificationSection() returns that section directly. However, records may or may not have a Processor Accounting Section. The method r30.processorAccountingSections() returns a List which contains 0 or 1 entry. This means that we can iterate over the list and it will happily process 0 sections from records without a processor accounting section. An explicit check for the number of sections isn’t required, and the method of processing can be consistent for all section types where the number of sections is variable.

Smf30Record r30 = new Smf30Record(record);
              
for (ProcessorAccountingSection proc : r30.processorAccountingSections())
{
Map<String, JobData> day = jobsByDay.get(recordDayOfWeek.getValue());
                  
    String jobname = r30.identificationSection().smf30jbn();
    JobData jobentry = day.get(jobname);
    if (jobentry == null)
    {
     day.put(jobname, new JobData(proc));
    }
    else
    {
     jobentry.add(proc);
    }
}

Collecting the CPU times

We collect each of CP, zIIP and zAAP time from the processor accounting section.

private static class JobData {
JobData(ProcessorAccountingSection proc)
    {
     add(proc);
    }
        
    public void add(ProcessorAccountingSection proc)
    {
     cpTime = cpTime.plus(proc.smf30cpt())
            .plus(proc.smf30cps())
            .minus(proc.smf30TimeOnIfa())
            .minus(proc.smf30TimeOnZiip());
        zaapTime = zaapTime.plus(proc.smf30TimeOnIfa());
        ziipTime ziipTime.plus(proc.smf30TimeOnZiip());
    }
        
    Duration cpTime = Duration.ZERO;
    Duration ziipTime = Duration.ZERO;
    Duration zaapTime = Duration.ZERO;
}

Producing the Output

The writeReport() method produces the output. We produce a report for each day of the week where we collected job information. For each day we sort the jobs by CP time using the same method as Sample 1, then output the top 10 jobs.

Accumulated CPU time is written in the format hhh:mm:ss. We create a helper method to format the java.time.duration as hhmmss.

private static String hhhmmss(Duration dur)
{
long hours = dur.toHours();
long minutes = dur.minus(Duration.ofHours(hours)).toMinutes();
long seconds = dur.minus(Duration.ofHours(hours))
.minus(Duration.ofMinutes(minutes)).toMillis() / 1000;
return String.format("%d:%02d:%02d", hours, minutes, seconds);
}

EasySMF:JE provides a helper method to divide one duration by another, e.g. when calculating CPU time as a percentage of elapsed time or one job CPU time as a percentage of total CPU time.
Utils.divideDurations(jobinfo.cpTime, totalCp) * 100
Output looks like:

Tuesday
Name             CPU  CPU%        zIIP        zAAP
CICSP        1:31:32    5%       00:08       00:00
DB2PDIST     1:30:34    5%     4:55:55       00:00
DB2PDBM1     1:02:36    4%       00:00       00:00
...

The Complete Java Program

package com.blackhillsoftware.samples;

import java.io.FileInputStream;
import java.io.IOException;
import java.time.*;
import java.util.*;
import java.util.Map.*;

import com.blackhillsoftware.smf.SmfRecord;
import com.blackhillsoftware.smf.SmfRecordReader;
import com.blackhillsoftware.smf.Utils;
import com.blackhillsoftware.smf.smf30.ProcessorAccountingSection;
import com.blackhillsoftware.smf.smf30.Smf30Record;

public class PrimeShiftTopJobs
{
public static void main(String[] args) throws IOException
{

LocalTime primeStart = LocalTime.of(8, 30);
LocalTime primeEnd = LocalTime.of(17, 30);

// We need a Jobname->JobData map for each day of the week.
// So we will create a List (array) of HashMap<String, JobData>.

List<HashMap<String, JobData>> jobsByDay =
new ArrayList<HashMap<String, JobData>>();

// Populate the list for each day of the week
// We need entries for days 1-7. Entry 0 is unused
for (int i = 0; i <= 7; i++)
{
jobsByDay.add(new HashMap<String, JobData>());
}

// Read and process the data

try (SmfRecordReader reader =
args.length == 0 ?
SmfRecordReader.fromDD("INPUT") :
SmfRecordReader.fromStream(new FileInputStream(args[0])))
{

for (SmfRecord record : reader)
{
// We are only interested in type 30 subtype 2 or 3
if (record.recordType() == 30
&& (record.subType() == 2 || record.subType() == 3))
{

DayOfWeek recordDayOfWeek = record.smfDate().getDayOfWeek();
LocalTime recordTime = record.smfTime();

// check if SMF record was created during prime shift
// Monday-Friday
if (recordDayOfWeek.getValue() >= DayOfWeek.MONDAY.getValue()
&& recordDayOfWeek.getValue() <= DayOfWeek.FRIDAY.getValue()
&& recordTime.isAfter(primeStart)
&& recordTime.isBefore(primeEnd))
{

// Create a type 30 record from the original record
Smf30Record r30 = new Smf30Record(record);

// Iterate over the processor accounting sections
// Since there is either 0 or 1 section in a record, this is
// just a convenient way to skip records without the section
for (ProcessorAccountingSection proc : r30
.processorAccountingSections())
{
// get collection of jobs for this day
Map<String, JobData> day = jobsByDay
.get(recordDayOfWeek.getValue());

String jobname = r30.identificationSection().smf30jbn();
JobData jobentry = day.get(jobname);
if (jobentry == null) // nothing for this jobname,
// create new
{
day.put(jobname, new JobData(proc));
}
else
{
jobentry.add(proc);
}
}
}
}
};
}

writeReport(jobsByDay);
}

private static void writeReport(List<HashMap<String, JobData>> dailyJobs)
{

for (DayOfWeek day : DayOfWeek.values())
{
// get the jobs for the day as a List so we can sort them
List<Entry<String, JobData>> jobs = new ArrayList<Entry<String, JobData>>(
dailyJobs.get(day.getValue()).entrySet());

if (jobs.size() > 0) // if we have data for this day
{
// sort list by CPU time descending
Collections.sort(jobs, new Comparator<Entry<String, JobData>>()
{
public int compare(Entry<String, JobData> s1,
Entry<String, JobData> s2)
{
return s2.getValue().cpTime.compareTo(s1.getValue().cpTime);
}
});

// calculate total CPU for the day
Duration totalCp = Duration.ZERO;
for (Entry<String, JobData> entry : jobs)
{
totalCp = totalCp.plus(entry.getValue().cpTime);
}

// write output for top 10 job s

// Headings
System.out.format("%n%s%n", day.toString());
System.out.format("%-8s %11s %5s %11s %11s %n", "Name", "CPU",
"CPU%", "zIIP", "zAAP");

int count = 0;
for (Entry<String, JobData> entry : jobs)
{
JobData jobinfo = entry.getValue();

// write detail line

System.out.format(
"%-8s %11s %4.0f%% %11s %11s %n",
entry.getKey(), // jobname
hhhmmss(jobinfo.cpTime),
Utils.divideDurations(jobinfo.cpTime, totalCp) * 100,
hhhmmss(jobinfo.ziipTime),
hhhmmss(jobinfo.zaapTime));

// limit to top 10 jobs
if (++count >= 10)
break;
}
}
}
}

private static String hhhmmss(Duration dur)
{
long hours = dur.toHours();
long minutes = dur.minus(Duration.ofHours(hours)).toMinutes();
long seconds = dur.minus(Duration.ofHours(hours))
.minus(Duration.ofMinutes(minutes)).toMillis() / 1000;
return String.format("%d:%02d:%02d", hours, minutes, seconds);
}

private static class JobData
{
JobData(ProcessorAccountingSection proc)
{
add(proc);
}

public void add(ProcessorAccountingSection proc)
{
cpTime = cpTime.plus(proc.smf30cpt()).plus(proc.smf30cps())
.minus(proc.smf30TimeOnIfa()).minus(proc.smf30TimeOnZiip());
zaapTime = zaapTime.plus(proc.smf30TimeOnIfa());
ziipTime = ziipTime.plus(proc.smf30TimeOnZiip());
}

Duration cpTime = Duration.ZERO;
Duration ziipTime = Duration.ZERO;
Duration zaapTime = Duration.ZERO;
}

}

Java SMF Sample 3 : Performance Index

This sample illustrates the use of functions provided in the type 72 classes to calculate the WLM performance index and work with WLM Importances.

Calculation of the performance index can be complex, because it depends on the type of goal: velocity, average response time or percentile response time. Methods are provided in the ServiceReportClassPeriodDataSection class so you don’t have to do it yourself. The performance index can be calculated from single or multiple sections with the same goal. Methods are also provided to calculate velocity.

Processing

The sample program reads the SMF data and builds a list of service class intervals which have a performance index greater than 2. The list is sorted and grouped by system, time and importance, then listed by performance index and service class name.

Type 72 records can have multiple Service/Report Class Period Data sections. This is handled with nested for… loops. To get the sections we are interested in:

  1. Read the record.
  2. Check the type and subtype.
  3. Create the type 72 record.
  4. Skip report classes – we will only report service classes. workloadManagerControlSection().r723mrcl() tells us whether this record relates to a report class.
  5. Loop through the list of sections.
for (SmfRecord record : reader)
{
if (record.recordType() == 72 && record.subType() == 3)
    {
     Smf72Record r72 = new Smf72Record(record);
        if (!r72.workloadManagerControlSection().r723mrcl())
        {
         for (ServiceReportClassPeriodDataSection section :
             r72.serviceReportClassPeriodDataSections())
            {
             // process section...
            }
        }
    }
};

The ServiceClassPeriod class is used to hold information about the entries to be included in the report. The time value comes from the time the SMF record was written. SMF records from a single interval may not have exactly the same time, so we add 30 seconds and truncate the value to a minute value. This ensures that all entries have the same times and the grouping by time works correctly (assuming that all records are written within 30 seconds of the minute boundary).

private static class ServiceClassPeriod
{
public ServiceClassPeriod(Smf72Record record,
ServiceReportClassPeriodDataSection section)
{
system = record.system();
// round to nearest minute
time = record.smfDateTime().plusSeconds(30)
.truncatedTo(ChronoUnit.MINUTES);
name = record.workloadManagerControlSection().r723mcnm();
period = section.r723cper();
importance = section.importance();
perfIndex = section.performanceIndex();
}

String system;
LocalDateTime time;
String name;
int period;
Importance importance;
double perfIndex;
}

For each section we check the performance index, and add it to the list if the performance index is greater than 2.

For velocity goals we also check the amount of activity. Sometimes velocity goals show a high performance index simply because they are doing very little work and have very few using samples. In this case, if the using + delay sample count for a velocity goal is less than 5% of the number of times WLM ran its sampling, we ignore it as a low activity class. These numbers are very arbitrary – you might choose to change them, or not use them at all.

// if performance index > 2 and:
if (section.performanceIndex() > 2 &&
// either not a velocity goal or
   //  using + delay is > 5% of total samples
    (!section.r723cvel() ||
    section.r723ctot() + section.r723ctou()  
     > r72.workloadManagerControlSection().r723mtvNum() / 20)
)
{
highPI.add(new ServiceClassPeriod(r72, section));
}

We then sort the entries by the various criteria that we want to group by, then write group headers and detail lines.

The ServiceReportClassPeriodDataSection class returns the importance as an Importance enum. When comparing Importance enums Importance.SYSTEM is greater than Importance.DISCRETIONARY, which means that Importances 1-5 sort opposite to their numeric order i.e. Importance.I1 is greater than Importance.I5.

Importance.toString() returns the importance name for reporting, e.g. “1”, “Discretionary”.

Output

The output looks like the following:

SYSA

   1/10/14 12:45 AM
      Importance: 1
         TSO      Period 1 4.0
      Importance: 4
         DB2T     Period 1 2.6

   1/10/14 1:00 AM
      Importance: 4
         DB2T     Period 1 4.6</pre>

The Complete Program

import java.io.FileInputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import com.blackhillsoftware.smf.Importance;
import com.blackhillsoftware.smf.SmfRecord;
import com.blackhillsoftware.smf.SmfRecordReader;
import com.blackhillsoftware.smf.smf72.Smf72Record;
import com.blackhillsoftware.smf.smf72.subtype3.ServiceReportClassPeriodDataSection;

public class PerformanceIndex
{
public static void main(String[] args) throws IOException
{
ArrayList<ServiceClassPeriod> highPI = new ArrayList<ServiceClassPeriod>();
try (SmfRecordReader reader =
args.length == 0 ?
SmfRecordReader.fromDD("INPUT") :
SmfRecordReader.fromStream(new FileInputStream(args[0])))
{
for (SmfRecord record : reader)
{
if (record.recordType() == 72 && record.subType() == 3)
{
Smf72Record r72 = new Smf72Record(record);

// skip report classes
if (!r72.workloadManagerControlSection().r723mrcl())
{
for (ServiceReportClassPeriodDataSection section : r72
.serviceReportClassPeriodDataSections())
{
// if performance index > 2 and:
if (section.performanceIndex() > 2
&&
// either not a velocity goal or
(!section.r723cvel() ||
// using + delay is > 5% of total samples
section.r723ctot() + section.r723ctou() >
r72.workloadManagerControlSection().r723mtvNum() / 20))
{
highPI.add(new ServiceClassPeriod(r72, section));
}
}
}
}
}
}
writeReport(highPI);
}

private static void writeReport(ArrayList<ServiceClassPeriod> highPI)
{
// Use custom comparator for complex sort order
Collections.sort(highPI, new ServiceClassPeriodComparator());

DateTimeFormatter timef = DateTimeFormatter.ISO_LOCAL_TIME;
DateTimeFormatter datef = DateTimeFormatter.ISO_LOCAL_DATE;

LocalDateTime currentTime = null;
String currentSystem = null;
Importance currentImportance = null;

for (ServiceClassPeriod scInfo : highPI)
{

// group by System
if (!scInfo.system.equals(currentSystem))
{
System.out.format("%n%s%n", scInfo.system);
currentSystem = scInfo.system;
currentTime = null;
currentImportance = null;
}
if (!scInfo.time.equals(currentTime))
{
System.out.format("%n %s %s%n", scInfo.time.format(datef),
scInfo.time.format(timef));
currentTime = scInfo.time;
currentImportance = null;
}
// Then by importance
if (scInfo.importance != currentImportance)
{
System.out.format(" Importance: %s%n", scInfo.importance);
currentImportance = scInfo.importance;
}
// detail line
System.out.format(" %-8s Period %s %3.1f%n", scInfo.name,
scInfo.period, scInfo.perfIndex);
}
}

/**
*
* Class to keep information about a service class period
*
*/
private static class ServiceClassPeriod
{
public ServiceClassPeriod(Smf72Record record,
ServiceReportClassPeriodDataSection section)
{
system = record.system();
// round to nearest minute
time = record.smfDateTime().plusSeconds(30)
.truncatedTo(ChronoUnit.MINUTES);
name = record.workloadManagerControlSection().r723mcnm();
period = section.r723cper();
importance = section.importance();
perfIndex = section.performanceIndex();
}

String system;
LocalDateTime time;
String name;
int period;
Importance importance;
double perfIndex;
}

/**
*
* Comparator to implement custom sort order for report
*/
private static class ServiceClassPeriodComparator implements
Comparator<ServiceClassPeriod>
{
// sort by system,
// then by time,
// then by importance
// then by performance index descending
// finally by name and period
public int compare(ServiceClassPeriod s1, ServiceClassPeriod s2)
{
int result = s1.system.compareTo(s2.system);
if (result != 0)
return result;
result = s1.time.compareTo(s2.time);
if (result != 0)
return result;
result = s2.importance.compareTo(s1.importance);
if (result != 0)
return result;
// reversed to sort descending
result = Double.compare(s2.perfIndex, s1.perfIndex);
if (result != 0)
return result;
result = s1.name.compareTo(s2.name);
if (result != 0)
return result;
result = Integer.compare(s1.period, s2.period);
return result;
}
}
}

Processing z/OS SMF data using Java

Black Hill Software is currently developing an API for processing SMF data using Java.

The API builds on experience gained developing EasySMF to create a powerful and easy to use API for working with SMF data.

The API will run on z/OS and other platforms such as Linux and Windows. The API is 100% Java which makes it zAAP eligible on z/OS.

The design at the time of this post used Joda Time for times and durations. This was changed prior to the initial release to use the java.time package introduced with Java 8. Java.time has the same advantages as Joda Time but it is a core part of Java so the removal of an external dependency was worth the cost of requiring Java 8 as a minimum level. Java.time is well suited to working with the dates and times in SMF, with specific classes to distinguish between e.g. local date/times, UTC date/times and durations. The java.time resolution of 1 nanosecond is accurate enough for most SMF purposes.

Data Conversions

The API aims to provide a consistent interface across different record types and sections.

  • Times representing a duration e.g. CPU time, connect time are converted to floating point values expressed in seconds. This allows easy manipulation and comparison, without dealing with the multitude of time units used in SMF. The raw unconverted value is also available if required.
  • Dates and times of day are expressed using Joda-Time classes. Joda-Time provides simple functions for most date and time calculations, so you can easily work with days of the week etc. It also provides time zone calculations, so you can e.g. apply a time zone to the times from the SMF records and match times between systems in different time zones. Joda-Time includes the Time Zone Database, so if you apply a time zone it can apply the correct rules to account for daylight savings.
  • STCK fields are available as a DateTime value (with only millisecond precision) and as a BigInteger with the complete value for use as a timestamp, for sorting etc.
  • 1, 2 and 3 byte integer values are converted to the Java int datatype.
  • 4 byte/32 bit unsigned integer values are converted to the Java long (64 bit integer) datatype. Java does not have an unsigned datatype, so a 4 byte int can’t hold the complete range of 32 bit unsigned values.
  • 8 byte/64 bit unsigned integer values are available as long or BigInteger values. The long datatype may provide better performance if the value will not exceed the maximum 64 bit signed value. The API will throw an exception if the value is too large for a long. If this is possible, use the BigInteger value.
  • Integers greater than 8 bytes are converted to BigIntegers.
  • Floating point values are converted to the Java double datatype.
  • SMF record classes provide methods to return the SMF subsections. Subsections are returned as a List<T> if the number of sections can vary, or individually if there is always a single section.

Current Status

The API is currently undergoing pre-release testing. I am looking for feedback on usability, performance and naming conventions. You can download the current version from the links on this page. Feedback is welcome in the comments area, or to info@blackhillsoftware.com.

Record types 30, 70, 71, 72, 73, 74, 75, 77 and 78 are currently mapped. More record types are planned, but these record types should make enough interesting data available for some realistic testing and real world feedback on the design of the API.

At this point the API design is changeable, so program changes may be required for new versions. One of the objectives of the testing period is to get enough feedback to identify problems now, and avoid breaking changes in the future.

Feedback

Some of the areas where I would particularly like feedback are:

  • Naming conventions: Many Java APIs use the get…() naming convention for method names. Almost all the SMF methods are get methods. Prefixing everything with get didn’t seem to add much value and made the resulting code more difficult to read, so the methods currently use bare field names. Are there advantages to using the get prefix e.g. for use with code generation/reporting tools?
  • More naming conventions: Many of the names of classes representing section types are long. However, they are named to be consistent with the SMF documentation. I believe this makes the programs easier to write and understand, and makes the Eclipse auto complete easier to use.
  • Use of the Joda Time classes: Joda Time is useful for date and time calculations. However, it does add a dependency on an external library. Would it be preferable to use the standard date time classes and eliminate the dependency? Joda Time could still be used at a site’s own discretion, writing their own conversions from standard times as required.
  • General comments on the data types used.

Documentation and Downloads

The current Javadoc is available here.

For more information, including code samples see:

https://www.blackhillsoftware.com/news/category/java-smf/

Sample Programs

Sample 1 : SMF Records by type and subtype

Sample 2 : Prime Shift Top CPU Consumers

Sample 3 : Performance Index

Download the current version here:

Samples are available on Github: https://github.com/BlackHillSoftware/easysmf-samples