/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.performance.tests; import com.android.ddmlib.IDevice; import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner; import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; import com.android.tradefed.config.Option; import com.android.tradefed.config.Option.Importance; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.device.LogcatReceiver; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.result.CollectingTestListener; import com.android.tradefed.result.FileInputStreamSource; import com.android.tradefed.result.ITestInvocationListener; import com.android.tradefed.result.InputStreamSource; import com.android.tradefed.result.LogDataType; import com.android.tradefed.result.TestResult; import com.android.tradefed.testtype.IDeviceTest; import com.android.tradefed.testtype.IRemoteTest; import com.android.tradefed.util.FileUtil; import com.android.tradefed.util.ListInstrumentationParser; import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget; import com.android.tradefed.util.StreamUtil; import com.android.tradefed.util.proto.TfMetricProtoUtil; import org.junit.Assert; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * To test the app launch performance for the list of activities present in the given target package * or the custom list of activities present in the target package.Activities are launched number of * times present in the launch count. Launch time is analyzed from the logcat data and more detailed * timing(section names) is analyzed from the atrace files captured when launching each activity. */ public class HermeticLaunchTest implements IRemoteTest, IDeviceTest { private static enum AtraceSectionOptions { LAYOUT("layout"), DRAW("draw"), BINDAPPLICATION("bindApplication"), ACTIVITYSTART("activityStart"), ONCREATE("onCreate"), INFLATE("inflate"); private final String name; private AtraceSectionOptions(String s) { this.name = s; } @Override public String toString() { return name; } } private static final String TOTALLAUNCHTIME = "totalLaunchTime"; private static final String LOGCAT_CMD = "logcat -v threadtime " + "ActivityManager:* ActivityTaskManager:* *:s"; private static final String LAUNCH_PREFIX = "^\\d*-\\d*\\s*\\d*:\\d*:\\d*.\\d*\\s*\\d*\\s*" + "\\d*\\s*I (ActivityTaskManager|ActivityManager): Displayed\\s*"; private static final String LAUNCH_SUFFIX = ":\\s*\\+(?.[a-zA-Z\\d]*)\\s*" + "(?.*)\\s*$"; private static final Pattern LAUNCH_ENTRY = Pattern.compile( "^\\d*-\\d*\\s*\\d*:\\d*:\\d*." + "\\d*\\s*\\d*\\s*\\d*\\s*I (ActivityTaskManager|ActivityManager):" + " Displayed\\s*(?.*)\\s*$"); private static final Pattern TRACE_ENTRY1 = Pattern.compile( "^[^-]*-(?\\d+)\\s+\\[\\d+\\]\\s+\\S{4}\\s+" + "(?\\d+)\\.(?\\d+):\\s+(?.*)\\s*$"); private static final Pattern TRACE_ENTRY2 = Pattern.compile( "^[^-]*-(?\\d+)\\s*\\(\\s*\\d*-*\\)\\s*\\[\\d+\\]\\s+\\S{4}\\s+" + "(?\\d+)\\.(?\\d+):\\s+(?.*)\\s*$"); private static final Pattern ATRACE_BEGIN = Pattern.compile("tracing_mark_write: B\\|(?\\d+)\\|(?.+)"); // Matches new and old format of END time stamp. // rformanceLaunc-6315 ( 6315) [007] ...1 182.622217: tracing_mark_write: E|6315 // rformanceLaunc-6315 ( 6315) [007] ...1 182.622217: tracing_mark_write: E private static final Pattern ATRACE_END = Pattern.compile("tracing_mark_write: E\\|*(?\\d*)"); private static final Pattern ATRACE_COUNTER = Pattern.compile( "tracing_mark_write: C\\|(?\\d+)\\|(?[^|]+)\\|(?\\d+)"); private static final Pattern ATRACE_HEADER_ENTRIES = Pattern.compile( "# entries-in-buffer/entries-written:\\s+(?\\d+)/" + "(?\\d+)\\s+#P:\\d+\\s*"); private static final int LOGCAT_SIZE = 20971520; // 20 mb private static final long SEC_TO_MILLI = 1000; private static final long MILLI_TO_MICRO = 1000; @Option(name = "runner", description = "The instrumentation test runner class name to use.") private String mRunnerName = ""; @Option( name = "package", shortName = 'p', description = "The manifest package name of the Android test application to run.", importance = Importance.IF_UNSET) private String mPackageName = "com.android.performanceapp.tests"; @Option( name = "target-package", description = "package which contains all the " + "activities to launch") private String mtargetPackage = null; @Option( name = "activity-names", description = "Fully qualified activity " + "names separated by comma" + "If not set then all the activities will be included for launching") private String mactivityNames = ""; @Option(name = "launch-count", description = "number of time to launch the each activity") private int mlaunchCount = 10; @Option(name = "trace-category", description = "comma separated list of trace categories") private String mtraceCategory = "am,view,gfx,dalvik"; @Option(name = "save-atrace", description = "Upload the atrace file in permanent storage") private boolean mSaveAtrace = false; @Option( name = "atrace-section", description = "Section to be parsed from atrace file. " + "This option can be repeated") private Set mSectionOptionSet = new HashSet<>(); @Option(name = "instantapp-url", description = "URL used to launch instant app") private String mInstantAppUrl = ""; @Option( name = "isolated-storage", description = "If set to false, the '--no-isolated-storage' flag will be passed to the am " + "instrument command. Only works for Q or later." ) private boolean mIsolatedStorage = true; private ITestDevice mDevice = null; private IRemoteAndroidTestRunner mRunner; private LogcatReceiver mLogcat; private Set mSectionSet = new HashSet<>(); private Map mActivityTraceFileMap; private Map> mActivityTimeResultMap = new HashMap<>(); private Map activityErrMsg = new HashMap<>(); private ListInstrumentationParser mListInstrumentationParser = null; @Override public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { mLogcat = new LogcatReceiver(getDevice(), LOGCAT_CMD, LOGCAT_SIZE, 0); mLogcat.start(); try { if (mSectionOptionSet.isEmpty()) { // Default sections mSectionOptionSet.add(AtraceSectionOptions.LAYOUT); mSectionOptionSet.add(AtraceSectionOptions.DRAW); mSectionOptionSet.add(AtraceSectionOptions.BINDAPPLICATION); mSectionOptionSet.add(AtraceSectionOptions.ACTIVITYSTART); mSectionOptionSet.add(AtraceSectionOptions.ONCREATE); mSectionOptionSet.add(AtraceSectionOptions.INFLATE); } else if (mSectionOptionSet.contains(AtraceSectionOptions.LAYOUT)) { // If layout is added, draw should also be included mSectionOptionSet.add(AtraceSectionOptions.DRAW); } for (AtraceSectionOptions sectionOption : mSectionOptionSet) { mSectionSet.add(sectionOption.toString()); } // Remove if there is already existing atrace_logs folder mDevice.executeShellCommand("rm -rf ${EXTERNAL_STORAGE}/atrace_logs"); if (mRunnerName.isEmpty()) { mRunnerName = queryRunnerName(); } mRunner = createRemoteAndroidTestRunner(mPackageName, mRunnerName, mDevice.getIDevice()); CollectingTestListener collectingListener = new CollectingTestListener(); mDevice.runInstrumentationTests(mRunner, collectingListener); Collection testResultsCollection = collectingListener.getCurrentRunResults().getTestResults().values(); List testResults = new ArrayList<>(testResultsCollection); /* * Expected Metrics : Map of = */ mActivityTraceFileMap = testResults.get(0).getMetrics(); Assert.assertTrue( "Unable to get the path to the trace files stored in the device", (mActivityTraceFileMap != null && !mActivityTraceFileMap.isEmpty())); // Analyze the logcat data to get total launch time analyzeLogCatData(mActivityTraceFileMap.keySet()); } finally { // Stop the logcat mLogcat.stop(); } // Analyze the atrace data to get bindApplication,activityStart etc.. analyzeAtraceData(listener); // Report the metrics to dashboard reportMetrics(listener); } /** * Report run metrics by creating an empty test run to stick them in. * * @param listener The {@link ITestInvocationListener} of test results */ private void reportMetrics(ITestInvocationListener listener) { for (String activityName : mActivityTimeResultMap.keySet()) { // Get the activity name alone from pkgname.activityname String[] activityNameSplit = activityName.split("\\."); if (!activityErrMsg.containsKey(activityName)) { Map activityMetrics = mActivityTimeResultMap.get(activityName); if (activityMetrics != null && !activityMetrics.isEmpty()) { CLog.v("Metrics for the activity : %s", activityName); for (String sectionName : activityMetrics.keySet()) { CLog.v( String.format( "Section name : %s - Time taken : %s", sectionName, activityMetrics.get(sectionName))); } listener.testRunStarted( activityNameSplit[activityNameSplit.length - 1].trim(), 0); listener.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(activityMetrics)); } } else { listener.testRunStarted(activityNameSplit[activityNameSplit.length - 1].trim(), 0); listener.testRunFailed(activityErrMsg.get(activityName)); } } } /** * Method to create the runner with given list of arguments * * @return the {@link IRemoteAndroidTestRunner} to use. * @throws DeviceNotAvailableException */ IRemoteAndroidTestRunner createRemoteAndroidTestRunner( String packageName, String runnerName, IDevice device) throws DeviceNotAvailableException { RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(packageName, runnerName, device); runner.addInstrumentationArg("targetpackage", mtargetPackage); runner.addInstrumentationArg("launchcount", mlaunchCount + ""); runner.addInstrumentationArg("tracecategory", mtraceCategory); if (!mInstantAppUrl.isEmpty()) { runner.addInstrumentationArg("instanturl", mInstantAppUrl); } if (mactivityNames != null && !mactivityNames.isEmpty()) { runner.addInstrumentationArg("activitylist", mactivityNames); } if (!mSaveAtrace) { runner.addInstrumentationArg("recordtrace", "false"); } String runOptions = ""; // isolated-storage flag only exists in Q and after. if (!mIsolatedStorage && getDevice().checkApiLevelAgainstNextRelease(29)) { runOptions += "--no-isolated-storage "; } runner.setRunOptions(runOptions); return runner; } /** * Get the {@link ListInstrumentationParser} used to parse 'pm list instrumentation' queries. */ protected ListInstrumentationParser getListInstrumentationParser() { if (mListInstrumentationParser == null) { mListInstrumentationParser = new ListInstrumentationParser(); } return mListInstrumentationParser; } /** * Query the device for a test runner to use. * * @return the first test runner name that matches the package or null if we don't find any. * @throws DeviceNotAvailableException */ protected String queryRunnerName() throws DeviceNotAvailableException { ListInstrumentationParser parser = getListInstrumentationParser(); getDevice().executeShellCommand("pm list instrumentation", parser); for (InstrumentationTarget target : parser.getInstrumentationTargets()) { if (mPackageName.equals(target.packageName)) { return target.runnerName; } } throw new RuntimeException( String.format("Unable to determine runner name for package: %s", mPackageName)); } /** * To analyze the log cat data to get the display time reported by activity manager during the * launches activitySet is set of activityNames returned as a part of testMetrics from the * device */ public void analyzeLogCatData(Set activitySet) { Map> amLaunchTimes = new HashMap<>(); Map activityPatternMap = new HashMap<>(); Matcher match = null; String line; /* * Sample line format in logcat 06-17 16:55:49.6 60 642 I * (ActivityTaskManager|ActivityManager): Displayed pkg/.activity: +Tms (total +9s9ms) */ for (String activityName : activitySet) { int lastIndex = activityName.lastIndexOf("."); /* * actvitySet has set of activity names in the format packageName.activityName logcat * has the format packageName/.activityName --> activityAlias */ String activityAlias = new String(); if (mInstantAppUrl.isEmpty()) { activityAlias = activityName.subSequence(0, lastIndex) + "/" + activityName.subSequence(lastIndex, activityName.length()); } else { activityAlias = mtargetPackage + ".*"; } String finalPattern = LAUNCH_PREFIX + activityAlias + LAUNCH_SUFFIX; activityPatternMap.put(Pattern.compile(finalPattern), activityName); } try (InputStreamSource input = mLogcat.getLogcatData(); BufferedReader br = new BufferedReader(new InputStreamReader(input.createInputStream()))) { while ((line = br.readLine()) != null) { /* * Launch entry needed otherwise we will end up in comparing all the lines for all * the patterns */ if ((match = matches(LAUNCH_ENTRY, line)) != null) { for (Pattern pattern : activityPatternMap.keySet()) { if ((match = matches(pattern, line)) != null) { CLog.v("Launch Info : %s", line); int displayTimeInMs = extractLaunchTime(match.group("launchtime")); String activityName = activityPatternMap.get(pattern); if (amLaunchTimes.containsKey(activityName)) { amLaunchTimes.get(activityName).add(displayTimeInMs); } else { List launchTimes = new ArrayList<>(); launchTimes.add(displayTimeInMs); amLaunchTimes.put(activityName, launchTimes); } } } } } } catch (IOException io) { CLog.e(io); } // Verify logcat data for (String activityName : amLaunchTimes.keySet()) { Assert.assertEquals( "Data lost for launch time for the activity :" + activityName, amLaunchTimes.get(activityName).size(), mlaunchCount); } /* * Extract and store the average launch time data reported by activity manager for each * activity */ for (String activityName : amLaunchTimes.keySet()) { Double totalTime = 0d; for (Integer launchTime : amLaunchTimes.get(activityName)) { totalTime += launchTime; } Double averageTime = Double.valueOf(totalTime / amLaunchTimes.get(activityName).size()); if (mActivityTimeResultMap.containsKey(activityName)) { mActivityTimeResultMap .get(activityName) .put(TOTALLAUNCHTIME, String.format("%.2f", averageTime)); } else { Map launchTime = new HashMap<>(); launchTime.put(TOTALLAUNCHTIME, String.format("%.2f", averageTime)); mActivityTimeResultMap.put(activityName, launchTime); } } } /** * To extract the launch time displayed in given line * * @param duration * @return */ public int extractLaunchTime(String duration) { String formattedString = duration.replace("ms", ""); if (formattedString.contains("s")) { String[] splitString = formattedString.split("s"); int finalTimeInMs = Integer.parseInt(splitString[0]) * 1000; finalTimeInMs = finalTimeInMs + Integer.parseInt(splitString[1]); return finalTimeInMs; } else { return Integer.parseInt(formattedString); } } /** To analyze the trace data collected in the device during each activity launch. */ public void analyzeAtraceData(ITestInvocationListener listener) throws DeviceNotAvailableException { for (String activityName : mActivityTraceFileMap.keySet()) { try { // Get the list of associated filenames for given activity String filePathAll = mActivityTraceFileMap.get(activityName); Assert.assertNotNull( String.format( "Unable to find trace file paths for activity : %s", activityName), filePathAll); String[] filePaths = filePathAll.split(","); Assert.assertEquals( String.format( "Unable to find file path for all the launches " + "for the activity :%s", activityName), filePaths.length, mlaunchCount); // Pull and parse the info List>> mutipleLaunchTraceInfo = new LinkedList<>(); for (int count = 0; count < filePaths.length; count++) { File currentAtraceFile = pullAtraceInfoFile(filePaths[count]); String[] splitName = filePaths[count].split("-"); // Process id is appended to original file name Map> singleLaunchTraceInfo = parseAtraceInfoFile(currentAtraceFile, splitName[splitName.length - 1]); // Upload the file if needed if (mSaveAtrace) { try (FileInputStreamSource stream = new FileInputStreamSource(currentAtraceFile)) { listener.testLog(currentAtraceFile.getName(), LogDataType.TEXT, stream); } } // Remove the atrace files FileUtil.deleteFile(currentAtraceFile); mutipleLaunchTraceInfo.add(singleLaunchTraceInfo); } // Verify and Average out the aTrace Info and store it in result map averageAtraceData(activityName, mutipleLaunchTraceInfo); } catch (FileNotFoundException foe) { CLog.e(foe); activityErrMsg.put( activityName, "Unable to find the trace file for the activity launch :" + activityName); } catch (IOException ioe) { CLog.e(ioe); activityErrMsg.put( activityName, "Unable to read the contents of the atrace file for the activity :" + activityName); } } } /** * To pull the trace file from the device * * @param aTraceFile * @return * @throws DeviceNotAvailableException */ public File pullAtraceInfoFile(String aTraceFile) throws DeviceNotAvailableException { String dir = "${EXTERNAL_STORAGE}/atrace_logs"; File atraceFileHandler = null; atraceFileHandler = getDevice().pullFile(dir + "/" + aTraceFile); Assert.assertTrue("Unable to retrieve the atrace files", atraceFileHandler != null); return atraceFileHandler; } /** * To parse and find the time taken for the given section names in each launch * * @param currentAtraceFile * @param sectionSet * @param processId * @return * @throws FileNotFoundException,IOException */ public Map> parseAtraceInfoFile( File currentAtraceFile, String processId) throws FileNotFoundException, IOException { CLog.v("Currently parsing :" + currentAtraceFile.getName()); String line; BufferedReader br = null; br = new BufferedReader(new FileReader(currentAtraceFile)); LinkedList processStack = new LinkedList<>(); Map> sectionInfo = new HashMap<>(); while ((line = br.readLine()) != null) { // Skip extra lines that aren't part of the trace if (line.isEmpty() || line.startsWith("capturing trace...") || line.equals("TRACE:") || line.equals("done")) { continue; } // Header information Matcher match = null; // Check if any trace entries were lost if ((match = matches(ATRACE_HEADER_ENTRIES, line)) != null) { int buffered = Integer.parseInt(match.group("buffered")); int written = Integer.parseInt(match.group("written")); if (written != buffered) { CLog.w( String.format( "%d trace entries lost for the file %s", written - buffered, currentAtraceFile.getName())); } } else if ((match = matches(TRACE_ENTRY1, line)) != null || (match = matches(TRACE_ENTRY2, line)) != null) { /* * Two trace entries because trace format differs across devices <...>-tid [yyy] * ...1 zzz.ttt: tracing_mark_write: B|xxxx|tag_name pkg.name ( tid) [yyy] ...1 * zzz.tttt: tracing_mark_write: B|xxxx|tag_name */ long timestamp = SEC_TO_MILLI * Long.parseLong(match.group("secs")) + Long.parseLong(match.group("usecs")) / MILLI_TO_MICRO; // Get the function name from the trace entry String taskId = match.group("tid"); String function = match.group("function"); // Analyze the lines that matches the processid if (!taskId.equals(processId)) { continue; } if ((match = matches(ATRACE_BEGIN, function)) != null) { // Matching pattern looks like tracing_mark_write: B|xxxx|tag_name String sectionName = match.group("name"); // Push to the stack processStack.add(new TraceRecord(sectionName, taskId, timestamp)); } else if ((match = matches(ATRACE_END, function)) != null) { /* * Matching pattern looks like tracing_mark_write: E Pop from the stack when end * reaches */ String endProcId = match.group("procid"); if (endProcId.isEmpty() || endProcId.equals(processId)) { TraceRecord matchingBegin = processStack.removeLast(); if (mSectionSet.contains(matchingBegin.name)) { if (sectionInfo.containsKey(matchingBegin.name)) { SectionPeriod newSecPeriod = new SectionPeriod(matchingBegin.timestamp, timestamp); CLog.v( "Section :%s took :%f msecs ", matchingBegin.name, newSecPeriod.duration); sectionInfo.get(matchingBegin.name).add(newSecPeriod); } else { List infoList = new LinkedList<>(); SectionPeriod newSecPeriod = new SectionPeriod(matchingBegin.timestamp, timestamp); CLog.v( String.format( "Section :%s took :%f msecs ", matchingBegin.name, newSecPeriod.duration)); infoList.add(newSecPeriod); sectionInfo.put(matchingBegin.name, infoList); } } } } else if ((match = matches(ATRACE_COUNTER, function)) != null) { // Skip this for now. May want to track these later if needed. } } } StreamUtil.close(br); return sectionInfo; } /** * To take the average of the multiple launches for each activity * * @param activityName * @param mutipleLaunchTraceInfo */ public void averageAtraceData( String activityName, List>> mutipleLaunchTraceInfo) { String verificationResult = verifyAtraceMapInfo(mutipleLaunchTraceInfo); if (verificationResult != null) { CLog.w( "Not all the section info captured for the activity :%s. Missing: %s. " + "Please go to atrace file to look for detail.", activityName, verificationResult); } Map launchSum = new HashMap<>(); for (String sectionName : mSectionSet) { launchSum.put(sectionName, 0d); } for (Map> singleLaunchInfo : mutipleLaunchTraceInfo) { for (String sectionName : singleLaunchInfo.keySet()) { for (SectionPeriod secPeriod : singleLaunchInfo.get(sectionName)) { if (sectionName.equals(AtraceSectionOptions.DRAW.toString())) { // Get the first draw time for the launch Double currentSum = launchSum.get(sectionName) + secPeriod.duration; launchSum.put(sectionName, currentSum); break; } // Sum the multiple layout times before the first draw in this launch if (sectionName.equals(AtraceSectionOptions.LAYOUT.toString())) { Double drawStartTime = singleLaunchInfo .get(AtraceSectionOptions.DRAW.toString()) .get(0) .startTime; if (drawStartTime < secPeriod.startTime) { break; } } Double currentSum = launchSum.get(sectionName) + secPeriod.duration; launchSum.put(sectionName, currentSum); } } } // Update the final result map for (String sectionName : mSectionSet) { Double averageTime = launchSum.get(sectionName) / mutipleLaunchTraceInfo.size(); mActivityTimeResultMap .get(activityName) .put(sectionName, String.format("%.2f", averageTime)); } } /** * To check if all the section info caught for all the app launches * * @param multipleLaunchTraceInfo * @return String: the missing section name, null if no section info missing. */ public String verifyAtraceMapInfo( List>> multipleLaunchTraceInfo) { for (Map> singleLaunchInfo : multipleLaunchTraceInfo) { Set testSet = new HashSet<>(mSectionSet); testSet.removeAll(singleLaunchInfo.keySet()); if (testSet.size() != 0) { return testSet.toString(); } } return null; } /** * Checks whether {@code line} matches the given {@link Pattern}. * * @return The resulting {@link Matcher} obtained by matching the {@code line} against * {@code pattern}, or null if the {@code line} does not match. */ private static Matcher matches(Pattern pattern, String line) { Matcher ret = pattern.matcher(line); return ret.matches() ? ret : null; } @Override public void setDevice(ITestDevice device) { mDevice = device; } @Override public ITestDevice getDevice() { return mDevice; } /** * A record to keep track of the section start time,end time and the duration in milliseconds. */ public static class SectionPeriod { private double startTime; private double endTime; private double duration; public SectionPeriod(double startTime, double endTime) { this.startTime = startTime; this.endTime = endTime; this.duration = endTime - startTime; } public double getStartTime() { return startTime; } public void setStartTime(long startTime) { this.startTime = startTime; } public double getEndTime() { return endTime; } public void setEndTime(long endTime) { this.endTime = endTime; } public double getDuration() { return duration; } public void setDuration(long duration) { this.duration = duration; } } /** * A record of a trace event. Includes the name of the section, and the time that the event * occurred (in milliseconds). */ public static class TraceRecord { private String name; private String processId; private double timestamp; /** * Construct a new {@link TraceRecord} with the given {@code name} and {@code timestamp} . */ public TraceRecord(String name, String processId, long timestamp) { this.name = name; this.processId = processId; this.timestamp = timestamp; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getProcessId() { return processId; } public void setProcessId(String processId) { this.processId = processId; } public double getTimestamp() { return timestamp; } public void setTimestamp(long timestamp) { this.timestamp = timestamp; } } }