Index: script/bootchartd =================================================================== RCS file: /cvsroot/bootchart/bootchart/script/bootchartd,v retrieving revision 1.12 diff -u -r1.12 bootchartd --- script/bootchartd 5 Jul 2005 20:13:51 -0000 1.12 +++ script/bootchartd 11 Oct 2005 10:37:07 -0000 @@ -20,11 +20,15 @@ VERSION="0.8" # Read configuration. -if [ -f /etc/bootchartd.conf ]; then - . /etc/bootchartd.conf +if [ -f $PWD/bootchartd.conf ]; then + . $PWD/bootchartd.conf else - echo "/etc/bootchartd.conf missing" - exit 1 + if [ -f /etc/bootchartd.conf ]; then + . /etc/bootchartd.conf + else + echo "/etc/bootchartd.conf missing" >&2 + exit 1 + fi fi # The path to GNU accounting utilities (optional) @@ -71,6 +75,7 @@ elif [ "$#" -gt 0 ]; then # If a command was passed, run it # (used for profiling specific applications) + sleep $SAMPLE_PERIOD $@ echo "profile.process = $@" >> "$TMPFS_MOUNT"/header stop @@ -87,12 +92,18 @@ local logfile="$2" while [ -f "$BOOTLOG_LOCK" ]; do # Write the time (in jiffies). - read cpustat < /proc/stat - get_uptime $cpustat + read uptime < /proc/uptime + uptime=${uptime%% [0-9]*} + uptime=${uptime/./} + echo $uptime # Log the command output if [ "$cmd" = "__proc_stat__" ]; then - cat /proc/*/stat 2>/dev/null + if [ "$SINGLE_UID" = "yes" ]; then + find /proc/[0-9]* -noleaf -mindepth 1 -maxdepth 1 -user $EUID -name stat 2> /dev/null | xargs cat 2> /dev/null + else + cat /proc/*/stat 2>/dev/null + fi else $cmd 2>/dev/null fi @@ -135,10 +146,9 @@ done; } - # Stop the boot logger. The lock file is removed to force the loggers in # background to exit. Some final log files are created and then all log files -# from the tmpfs are packaged and stored in /var/log. +# from the tmpfs are packaged and stored in $LOG_DIR. stop() { if [ ! -f "$BOOTLOG_LOCK" ]; then @@ -165,7 +175,7 @@ # Render the chart if configured (and the renderer is installed) [ "$AUTO_RENDER" = "yes" -a -x /usr/bin/bootchart ] && \ - /usr/bin/bootchart -o /var/log/ -f $AUTO_RENDER_FORMAT + /usr/bin/bootchart -o $LOG_DIR/ -f $AUTO_RENDER_FORMAT } @@ -181,7 +191,7 @@ elif [ -f /etc/SuSE-release ]; then echo "system.release = $( sed q /etc/SuSE-release )" elif [ -f /etc/debian_version ]; then - echo "system.release = Debian GNU/$( uname -s ) $( cat /etc/debian_version )" >> "$HEADER" + echo "system.release = Debian GNU/$( uname -s ) $( cat /etc/debian_version )" else echo "system.release = $( sed 's/\\.//g;q' /etc/issue )" fi Index: src/org/bootchart/Main.java =================================================================== RCS file: /cvsroot/bootchart/bootchart/src/org/bootchart/Main.java,v retrieving revision 1.13 diff -u -r1.13 Main.java --- src/org/bootchart/Main.java 5 Jul 2005 20:07:03 -0000 1.13 +++ src/org/bootchart/Main.java 11 Oct 2005 10:37:07 -0000 @@ -47,8 +47,10 @@ import org.bootchart.common.BootStats; import org.bootchart.common.Common; import org.bootchart.common.ProcessTree; +import org.bootchart.common.PsStats; import org.bootchart.common.Stats; import org.bootchart.parser.HeaderParser; +import org.bootchart.parser.MilestonesParser; import org.bootchart.parser.linux.PacctParser; import org.bootchart.parser.linux.PidNameParser; import org.bootchart.parser.linux.ProcDiskStatParser; @@ -138,6 +140,8 @@ inputFiles.add(logTarball); } else if (logDir.exists()) { inputFiles.add(logDir); + } else { + System.err.println("/var/log/bootchart not found"); } } @@ -199,12 +203,13 @@ Properties headers = new Properties(); Map pidNameMap = null; - List processList = null; int samplePeriod = -1; Map forkMap = null; Stats cpuStats = null; Stats diskStats = null; + PsStats psStats = null; BootStats bootStats = null; + List milestonesList = null; int numCpu = 1; String monitoredApp = null; // used for profiling specific processes @@ -243,17 +248,11 @@ } else if (logName.equals("ps.log")) { // read ps log file - PsParser.PsStats psStats = - PsParser.parseLog(is, pidNameMap, forkMap); - processList = psStats.processList; - samplePeriod = psStats.samplePeriod; - + psStats = PsParser.parseLog(is, pidNameMap, forkMap); + } else if (logName.equals("proc_ps.log")) { // read the /proc/[PID]/stat log file - ProcPsParser.PsStats psStats = - ProcPsParser.parseLog(is, pidNameMap, forkMap); - processList = psStats.processList; - samplePeriod = psStats.samplePeriod; + psStats = ProcPsParser.parseLog(is, pidNameMap, forkMap); } else if (logName.equals("proc_stat.log")) { // read the /proc/stat log file @@ -267,6 +266,10 @@ // parse process forking PID mappings forkMap = PacctParser.parseLog(is); + } else if (logName.equals("milestones.log")) { + // parse boot process milestones + milestonesList = MilestonesParser.parseLog(is); + } else if (logName.equals("init_pidname.log")) { // map pids to command names (useful for Gentoo, where most init // processes are sourced and thus shown as "rc boot" or @@ -280,13 +283,12 @@ long opTime = 0; if (bootStats == null) { - ProcessTree procTree = - new ProcessTree(Collections.EMPTY_LIST, 0, monitoredApp, prune); - if (processList == null || processList.size() == 0) { + ProcessTree procTree = new ProcessTree(null, monitoredApp, prune); + if (psStats == null || psStats.processList.size() == 0) { log.warning("No process samples"); } else { opTime = System.currentTimeMillis(); - procTree = new ProcessTree(processList, samplePeriod, monitoredApp, prune); + procTree = new ProcessTree(psStats, monitoredApp, prune); opTime = System.currentTimeMillis() - opTime; log.fine("Tree generation and pruning took " + opTime + " ms"); @@ -295,7 +297,8 @@ } } //log.fine(procTree.toString()); - bootStats = new BootStats(cpuStats, diskStats, procTree); + MilestonesParser.adjustOffset(milestonesList, procTree.startTime); + bootStats = new BootStats(cpuStats, diskStats, procTree, milestonesList); } Renderer renderer = null; @@ -345,6 +348,7 @@ options.addOption("n", "no-prune", false, "do not prune the process tree"); + return options; } Index: src/org/bootchart/common/BootStats.java =================================================================== RCS file: /cvsroot/bootchart/bootchart/src/org/bootchart/common/BootStats.java,v retrieving revision 1.1 diff -u -r1.1 BootStats.java --- src/org/bootchart/common/BootStats.java 21 Jan 2005 00:24:19 -0000 1.1 +++ src/org/bootchart/common/BootStats.java 11 Oct 2005 10:37:07 -0000 @@ -19,6 +19,8 @@ */ package org.bootchart.common; +import java.util.List; + /** * BootStats encapsulates boot statistics. This includes global CPU and * disk I/O statistics and a process tree with process accounting. @@ -30,6 +32,8 @@ public Stats diskStats; /** The process tree.*/ public ProcessTree procTree; + /** The milestone list.*/ + public List milestonesList; /** * Creates a new boot statistics instance. @@ -38,9 +42,10 @@ * @param diskStats disk utilization and throughput I/O statistics * @param procTree the process tree */ - public BootStats(Stats cpuStats, Stats diskStats, ProcessTree procTree) { + public BootStats(Stats cpuStats, Stats diskStats, ProcessTree procTree, List milestonesList) { this.cpuStats = cpuStats; this.diskStats = diskStats; this.procTree = procTree; + this.milestonesList = milestonesList; } } Index: src/org/bootchart/common/ProcessTree.java =================================================================== RCS file: /cvsroot/bootchart/bootchart/src/org/bootchart/common/ProcessTree.java,v retrieving revision 1.10 diff -u -r1.10 ProcessTree.java --- src/org/bootchart/common/ProcessTree.java 10 Apr 2005 13:33:46 -0000 1.10 +++ src/org/bootchart/common/ProcessTree.java 11 Oct 2005 10:37:07 -0000 @@ -68,8 +68,9 @@ private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("mm:ss.SSS", Common.LOCALE); - /** The start time of the first process in the tree. */ + /** The start time of the graph */ public Date startTime; + public Date endTime; /** * The duration of the process tree (measured from the start time of @@ -78,15 +79,14 @@ */ public long duration; - /** Statistics sampling period. */ + /** The process statistics */ + private PsStats psStats; + private List processList; public int samplePeriod; /** The number of all processes in the tree. */ public int numProc; - - /** List of {@link Process} instances. */ - private List processList; - + /** The {@link Process} tree. */ public List processTree; @@ -94,23 +94,37 @@ * Creates a new process tree from the specified list of * Process instances. * - * @param processList list of process instances - * @param samplePeriod sampling period + * @param psStats process statistics * @param monitoredApp monitored application (or null if * the boot process is monitored) * @param prune whether to prune the tree by removing sleepy and * short-living processes and merging threads */ - public ProcessTree(List processList, int samplePeriod, String monitoredApp, - boolean prune) { - this.processList = processList; - this.samplePeriod = samplePeriod; - + public ProcessTree(PsStats psStats, String monitoredApp, boolean prune) { + this.psStats = psStats; + if(psStats == null || psStats.processList.size() == 0) { + this.processList = null; + return; + } + + this.processList = psStats.processList; + this.samplePeriod = psStats.samplePeriod; + if (processList == null || processList.size() == 0) { return; } - + + /** + * If we're monitoring an application, remove all processes that + * existed before the app started + */ + if(monitoredApp != null) { + int preExistingProcsRemoved = removePreExisting(); + log.fine("Removing " + preExistingProcsRemoved + " pre-existing processes"); + } + build(); +System.out.println("***** PROCESS TREE IS" + processTree + "\n*****"); log.fine("Number of all processes: " + numNodes(processTree)); /* @@ -147,15 +161,11 @@ build(); } - // compute the process tree times - startTime = getStartTime(processTree); - Date endTime = getEndTime(processTree); - duration = endTime.getTime() - startTime.getTime(); - /* Merge the logger's processes, since it forks lots of sleeps and * other processes. */ - int numRemoved = mergeLogger(processTree, LOGGER_PROC, monitoredApp); +int numRemoved = 0; +// int numRemoved = mergeLogger(processTree, LOGGER_PROC, monitoredApp); if (monitoredApp != null) { // profiling specific applications if (prune) { @@ -187,10 +197,20 @@ //log.fine(toString()); // update process tree times - startTime = getStartTime(processTree); - endTime = getEndTime(processTree); + if(monitoredApp == null) { + // Start time is time at which first process was started + startTime = getStartTime(processTree); + endTime = getEndTime(processTree); + } else { + // Start time is when stat collection started + startTime = psStats.startTime; + endTime = psStats.endTime; + } + duration = endTime.getTime() - startTime.getTime(); log.fine("Boot time: " + duration); +System.out.println(processList); +System.out.println(this); } /** @@ -201,7 +221,7 @@ for (Iterator i=processList.iterator(); i.hasNext(); ) { Process proc = (Process)i.next(); // find the parent process - if (proc.parent != null) { + if (proc.parent != null && processList.contains(proc.parent)) { proc.parent.childList.add(proc); } else { // a root process @@ -445,6 +465,7 @@ } private static void mergeProcesses(Process p1, Process p2) { +System.out.println("merging process " + p1.pid + "/" + p1.cmd + " with " + p2.pid + "/" + p2.cmd); p1.samples.addAll(p2.samples); long p1time = p1.startTime.getTime(); long p2time = p2.startTime.getTime(); @@ -487,6 +508,26 @@ } return numRemoved; } + + /** + * Removes processes that already existed when data collection started. + * Useful for when a specific application is being monitored. + * @return the number of removed processes + */ + private int removePreExisting() { + int numRemoved = 0; + + for(Iterator i = processList.iterator(); i.hasNext(); ) { + Process p = (Process) i.next(); + if(p.startTime.getTime() < psStats.startTime.getTime()) { +System.out.println("Removing process " + p.cmd); + i.remove(); + numRemoved++; + } + } + + return numRemoved; + } /** * Sort process tree. Index: src/org/bootchart/parser/linux/ProcPsParser.java =================================================================== RCS file: /cvsroot/bootchart/bootchart/src/org/bootchart/parser/linux/ProcPsParser.java,v retrieving revision 1.11 diff -u -r1.11 ProcPsParser.java --- src/org/bootchart/parser/linux/ProcPsParser.java 10 Apr 2005 13:33:54 -0000 1.11 +++ src/org/bootchart/parser/linux/ProcPsParser.java 11 Oct 2005 10:37:07 -0000 @@ -35,6 +35,7 @@ import org.bootchart.common.Common; import org.bootchart.common.Process; import org.bootchart.common.ProcessSample; +import org.bootchart.common.PsStats; import org.bootchart.common.Sample; @@ -46,14 +47,6 @@ */ public class ProcPsParser { private static final Logger log = Logger.getLogger(ProcPsParser.class.getName()); - - /** Process statistics. */ - public static class PsStats { - /** A list of processes (with enclosing CPU samples). */ - public List processList; - /** Statistics sampling period. */ - public int samplePeriod; - } /** * Parses the proc_ps.log file. The output from @@ -269,6 +262,8 @@ PsStats psStats = new PsStats(); psStats.processList = new ArrayList(processMap.values()); psStats.samplePeriod = samplePeriod; + psStats.startTime = startTime; + psStats.endTime = time; return psStats; } Index: src/org/bootchart/parser/linux/PsParser.java =================================================================== RCS file: /cvsroot/bootchart/bootchart/src/org/bootchart/parser/linux/PsParser.java,v retrieving revision 1.6 diff -u -r1.6 PsParser.java --- src/org/bootchart/parser/linux/PsParser.java 13 Feb 2005 21:57:06 -0000 1.6 +++ src/org/bootchart/parser/linux/PsParser.java 11 Oct 2005 10:37:07 -0000 @@ -35,6 +35,7 @@ import org.bootchart.common.Common; import org.bootchart.common.Process; import org.bootchart.common.ProcessSample; +import org.bootchart.common.PsStats; import org.bootchart.common.Sample; @@ -44,14 +45,6 @@ public class PsParser { private static final Logger log = Logger.getLogger(PsParser.class.getName()); - /** Process statistics. */ - public static class PsStats { - /** A list of processes (with enclosing CPU samples). */ - public List processList; - /** Statistics sampling period. */ - public int samplePeriod; - } - /** The mapping between ps column types and Java objects. */ private static Map COLUMN_TYPES = new HashMap(); @@ -247,6 +240,8 @@ } PsStats psStats = new PsStats(); psStats.processList = new ArrayList(processMap.values()); + psStats.startTime = startTime; + psStats.endTime = time; psStats.samplePeriod = samplePeriod; return psStats; } Index: src/org/bootchart/renderer/ImageRenderer.java =================================================================== RCS file: /cvsroot/bootchart/bootchart/src/org/bootchart/renderer/ImageRenderer.java,v retrieving revision 1.15 diff -u -r1.15 ImageRenderer.java --- src/org/bootchart/renderer/ImageRenderer.java 4 Jul 2005 12:27:19 -0000 1.15 +++ src/org/bootchart/renderer/ImageRenderer.java 11 Oct 2005 10:37:07 -0000 @@ -48,6 +48,7 @@ import org.bootchart.common.DiskTPutSample; import org.bootchart.common.DiskUtilSample; import org.bootchart.common.FileOpenSample; +import org.bootchart.common.Milestone; import org.bootchart.common.Process; import org.bootchart.common.ProcessSample; import org.bootchart.common.ProcessTree; @@ -129,6 +130,9 @@ private static final Stroke DEP_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10.0f, new float[]{2.0f, 2.0f}, 0.0f); + + /* How far the milestone is from the milestone text */ + private static final int MILESTONE_SEP = 16; /** Boot duration time format. */ private static final DateFormat BOOT_TIME_FORMAT = @@ -163,6 +167,7 @@ Stats diskStats = bootStats.diskStats; Stats cpuStats = bootStats.cpuStats; ProcessTree procTree = bootStats.procTree; + List milestonesList = bootStats.milestonesList; int headerH = 280; int barH = 55; @@ -170,10 +175,16 @@ int offX = 10; int offY = 10; + // Milestones + // HACK + int milestonesH = 0; + if(milestonesList != null) + milestonesH = milestonesList.size() * 16; + int secW = 25; // the width of a second int w = (int)(procTree.duration * secW / 1000) + 2*offX; int procH = 16; // the height of a process - int h = procH * procTree.numProc + headerH + 2*offY; + int h = procH * procTree.numProc + headerH + milestonesH + 2*offY; while (w > MAX_IMG_DIM && secW > 1) { secW /= 2; @@ -181,7 +192,7 @@ } while (h > MAX_IMG_DIM) { procH = procH * 3 / 4; - h = procH * procTree.numProc + headerH + 2*offY; + h = procH * procTree.numProc + headerH + milestonesH + 2*offY; } w = Math.min(w, MAX_IMG_DIM); @@ -615,6 +626,9 @@ setColor(g, BORDER_COLOR); g.drawRect(rectX, rectY, rectW, rectH); + + if(milestonesList != null) + drawMilestones(milestonesList, offX, procH * (procTree.numProc + 1) + headerH, secW); setColor(g, SIG_COLOR); g.setFont(SIG_FONT); @@ -631,7 +645,7 @@ img = img.getSubimage(0, 0, Math.max(w, titleW), h); } } - + private void drawHeader(Properties headers, int offX, long duration) { setColor(g, TEXT_COLOR); g.setFont(TITLE_FONT); @@ -792,6 +806,36 @@ g.drawString(label, sx, y + procH - 2); } + private void drawMilestones(List milestonesList, int startX, int startY, int secW) { + int currX, currY; + int textHeight = g.getFontMetrics(TEXT_FONT).getMaxAscent(); + + setColor(g, BORDER_COLOR); + currY = startY + textHeight / 2; + for(Iterator i = milestonesList.iterator(); i.hasNext(); ) { + Milestone m = (Milestone) i.next(); + float secs = (float) (m.timestamp.getTime() / 1000.0); + currX = (int) (startX + secs * secW); + + g.drawLine(currX, 280, currX, currY); + g.drawLine(currX, currY, currX + MILESTONE_SEP, currY); + currY += 16; + } + + setColor(g, TEXT_COLOR); + g.setFont(TEXT_FONT); + currY = startY + textHeight; + for(Iterator i = milestonesList.iterator(); i.hasNext(); ) { + Milestone m = (Milestone) i.next(); + String str = m.toString(); + float secs = (float) (m.timestamp.getTime() / 1000.0); + currX = (int) (startX + secs * secW); + + g.drawString(str, currX + MILESTONE_SEP, currY); + currY += 16; + } + } + /** * Sets the current color. If allowAlpha is not set (e.g. * for the EPS renderer), an opaque color is used. --- /dev/null 2005-10-10 14:55:46.672593000 +0200 +++ src/org/bootchart/common/Milestone.java 2005-08-21 15:27:33.000000000 +0200 @@ -0,0 +1,41 @@ +/* + * Bootchart -- Boot Process Visualization + * + * Copyright (C) 2004 Lorenzo Colitti + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package org.bootchart.common; + +import java.util.Date; +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +/** Milestone in the boot process */ +public class Milestone { + public Date timestamp; + public String description; + protected DateFormat dateFormat; + + public Milestone(Date timestamp, String description) { + this.timestamp = timestamp; + this.description = description; + this.dateFormat = new SimpleDateFormat("m:ss.S"); + } + + public String toString() { + return dateFormat.format(timestamp) + ": " + description; + } +} --- /dev/null 2005-10-10 14:55:46.672593000 +0200 +++ src/org/bootchart/common/PsStats.java 2005-08-12 21:20:07.000000000 +0200 @@ -0,0 +1,34 @@ +/* + * Bootchart -- Boot Process Visualization + * + * Copyright (C) 2004 Ziga Mahkovec + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package org.bootchart.common; + +import java.util.Date; +import java.util.List; + +/** Process statistics. */ +public class PsStats { + /** A list of processes (with enclosing CPU samples). */ + public List processList; + /** Statistics sampling period. */ + public int samplePeriod; + /** Start and end of sampling period. */ + public Date startTime; + public Date endTime; +} --- /dev/null 2005-10-10 14:55:46.672593000 +0200 +++ src/org/bootchart/parser/MilestonesParser.java 2005-08-21 20:30:23.000000000 +0200 @@ -0,0 +1,91 @@ +/* + * Bootchart -- Boot Process Visualization + * + * Copyright (C) 2005 Lorenzo Colitti + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package org.bootchart.parser; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.LinkedList; +import java.util.logging.Logger; + +import org.bootchart.common.Milestone; + +/** + * MilestonesParser parses the milestones log file, which contains + * information about chart milestones. + */ +public class MilestonesParser { + private static final Logger log = Logger.getLogger(MilestonesParser.class.getName()); + + /** + * Parses the milestones file. The file contains one line per milestone in the form: + * ssssssssss.NNNNNN milestone description string + * where ssssssssss.NNNNNN is a unix timestamp made up of seconds and nanoseconds. + * e.g: + * 1124551705.570623 starting boot process + * 1124551710.86122 network up + * + * @param reader the BufferedReader to read from + * @return a List of milestones + * @throws IOException if an I/O error occurs + */ + public static List parseLog(InputStream is) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + List milestonesList = new LinkedList(); + String line = reader.readLine(); + while(line != null) { + try { + /* Split timestamp from description */ + String dateStr = line.substring(0, line.indexOf(" ")); + String eventStr = line.substring(line.indexOf(" ") + 1); + + /* Split seconds from nanoseconds */ + float uptime = Float.parseFloat(dateStr); + long millis = (long) (uptime * 1000.0); + + Date date = new Date(millis); + + Milestone m = new Milestone(date, eventStr); + milestonesList.add(m); + log.fine("milestone at " + date + ": " + eventStr); + } catch(Exception e) { + } finally { + line = reader.readLine(); + } + } + return milestonesList; + } + + public static void adjustOffset(List milestonesList, Date date) { + if(milestonesList == null) + return; + + Iterator i = milestonesList.iterator(); + + while(i.hasNext()) { + Milestone m = (Milestone) i.next(); + m.timestamp = new Date( (m.timestamp.getTime() - date.getTime())); + } + } +}