diff --git a/assignment04/Agent.java b/assignment04/Agent.java index 26ccb27..f1324e2 100644 --- a/assignment04/Agent.java +++ b/assignment04/Agent.java @@ -8,11 +8,16 @@ * Carleton University * @version 1.0, January 07th, 2025 * @version 2.0, January 10th, 2026 + * + * @author Lavji, F + * @version 2.1, March 14, 2026 */ - public class Agent implements Runnable { - private AssemblyTable assemblyTable; //The common table between Agent and Technicians + private AssemblyTable assemblyTable; //The common table between Agent and Technicians + + // NEW --> Addition of Event Logger (Assignment04_Requirement01) + private final EventLogger logger = EventLogger.getInstance(); /** * Constructor for Agent @@ -24,25 +29,51 @@ public Agent(AssemblyTable t){ } /** - * Method used when Agent thread is ran + * Method used when Agent thread is ran. */ public void run(){ Components components1, components2; + + String name = Thread.currentThread().getName(); + logger.logEvent(name, "THREAD_START"); + System.out.println("[" + Thread.currentThread().getName() + "] Waiting to place first components on the table..."); - while (this.assemblyTable.getDronesAssembled() != 20){ //Will loop until 20 drones have been assembled - //Randomly selects two different components + + // Loop until MAX_DRONES have been assembled + while (this.assemblyTable.getDronesAssembled() != assemblyTable.getMaxDrones()) { + long startAttempt = System.currentTimeMillis(); + + // Randomly selects two different components components1 = Components.getRandomComponent(); components2 = Components.getRandomComponent(); - while (components1 == components2){ //If components are the same, select and new second component - components2 = Components.getRandomComponent(); - } - this.assemblyTable.addComponents(components1, components2); //Places the two selected components on the table + + // If components are the same, select and new second component + while (components1 == components2){ components2 = Components.getRandomComponent(); } + logger.logEvent(name, "COMPONENTS_SELECTED", + components1.toString() + ", " + components2.toString()); + + //Places the two selected components on the table + this.assemblyTable.addComponents(components1, components2); + logger.logEvent(name, "COMPONENTS_ADDED", + components1.toString() + ", " + components2.toString()); + // Sleep for between 0 and 5 seconds before calculating n! try { + logger.logEvent(name, "WORK_START"); Thread.sleep((int)(Math.random() * 3000)); + logger.logEvent(name, "WORK_END"); } catch (InterruptedException e) {} + + // NEW --> Metric Analysis (Assignment04_Requirement03) + long endAttempt = System.currentTimeMillis(); + long responseTime = endAttempt - startAttempt; + logger.logEventKV(name, "RESPONSE_TIME", "duration", String.valueOf(responseTime)); } + //All drones have been assembled - System.out.println("[" + Thread.currentThread().getName() + "] 20 drones assembled, ending..."); + logger.logEvent(name, "THREAD_END", + String.format("%d drones assembled, ending...", assemblyTable.getMaxDrones())); + System.out.println(String.format("[%s] %d drones assembled, ending...", + Thread.currentThread().getName(), assemblyTable.getMaxDrones())); } } diff --git a/assignment04/AssemblyTable.java b/assignment04/AssemblyTable.java index a9baf37..9cb9930 100644 --- a/assignment04/AssemblyTable.java +++ b/assignment04/AssemblyTable.java @@ -12,45 +12,70 @@ * Carleton University * @version 1.0, January 07th, 2025 * @version 2.0, January 10th, 2026 + * + * @author Lavji, F + * @version 2.1, March 14, 2026 */ public class AssemblyTable { - private final int SIZE = 2; //Capacity of table - private Components[] components = new Components[SIZE]; //List of components on the table - private boolean tableFull = false; //True if there is at least 1 component on the table - private int dronesMade = 0; //Running total of drones assembled + private final int SIZE = 2; //Capacity of table + private static final int MAX_DRONES = 20; + private Components[] components = new Components[SIZE]; //List of components on the table + private boolean tableFull = false; //True if there is at least 1 component on the table + private int dronesMade = 0; //Running total of drones assembled + + // NEW --> Addition of Event Logger (Assignment04_Requirement01) + private final EventLogger logger = EventLogger.getInstance(); /** - * Method used to allow an Agent to place components on the table when table is empty + * Method used to allow an Agent to place components on the table when table is empty. + * * @param components1 First component to be placed by Agent * @param components2 Second component to be placed by Agent */ public synchronized void addComponents(Components components1, Components components2) { - while (tableFull) { //Makes agent wait until table is empty to place components - if (this.dronesMade == 20){ //Will exit if no more drones are required to be assembled - return; - } + //Make agent wait until table is empty to place components + while (tableFull) { + logger.waitStart(Thread.currentThread().getName()); + + //Exit if no more drones are required to be assembled + if (this.dronesMade == MAX_DRONES){ return; } + try { wait(); //Tells agent to wait until notified - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - if (this.dronesMade == 20){ //Will exit if no more drones are required to be assembled - return; + logger.waitEnd(Thread.currentThread().getName()); + } catch (InterruptedException e) { e.printStackTrace(); } } + //Exit if no more drones are required to be assembled + if (this.dronesMade == MAX_DRONES){ return; } + //Components are placed on table components[0] = components1; components[1] = components2; + logger.logEventKV(Thread.currentThread().getName(), "PLACED_COMPONENTS", + "components", "[" + components1 + "," + components2 + "]", + "drones", String.valueOf(dronesMade)); + // Random delay to simulate real scenario try { + logger.logEvent(Thread.currentThread().getName(), "WORK_START"); Thread.sleep((int)(Math.random() * 1000)); + logger.logEvent(Thread.currentThread().getName(), "WORK_END"); } catch (InterruptedException e) {} tableFull = true; //Table is now full - System.out.println("[" + Thread.currentThread().getName() + "] " + components1.toString() + " and " + components2.toString() + " placed on the table."); - notifyAll(); //Notify all Technicians that table is full + logger.logEventKV(Thread.currentThread().getName(), "TABLE_FULL", + "components", "[" + components1 + "," + components2 + "]", + "drones", String.valueOf(dronesMade)); + + System.out.println("[" + Thread.currentThread().getName() + "] " + + components1.toString() + " and " + + components2.toString() + " placed on the table."); + + // Notify all Technicians that table is full + logger.logEvent(Thread.currentThread().getName(), "READY"); + notifyAll(); } /** @@ -58,48 +83,61 @@ public synchronized void addComponents(Components components1, Components compon * * @param components The component the Technician has an infinite supply of (Used to determine if Technician is eligible to take the components on the table) */ - public synchronized void getComponents(Components components) - { - while (!tableFull || componentsContains(components)) { //Makes Technician wait until the table is full and until the two required components from the Agent is available - if (this.dronesMade == 20){ //If 20 drones have been assembled, do not assemble another - return; - } + public synchronized void getComponents(Components components) { + + //Makes Technician wait until the table is full or until the two required components from the Agent is available + while (!canTake(components)) { + //If MAX_DRONES have been assembled, do not assemble another + if (this.dronesMade == MAX_DRONES){ return; } + try { + logger.waitStart(Thread.currentThread().getName()); wait(); //Make the Technician wait until notified that new components are available - } catch (InterruptedException e) { - e.printStackTrace(); - } + logger.waitEnd(Thread.currentThread().getName()); + } catch (InterruptedException e) { e.printStackTrace(); } } + logger.logEventKV(Thread.currentThread().getName(), "PICKED_UP", + "components", "[" + this.components[0] + "," + this.components[1] + "]", + "drones", String.valueOf(dronesMade)); + + System.out.println("[" + Thread.currentThread().getName() + "] Drone assembled."); System.out.println("[" + Thread.currentThread().getName() + "] Waiting for remaining components..."); this.dronesMade++; //Increase running total of drones assembled + logger.logEventKV(Thread.currentThread().getName(), "DRONE_ASSEMBLED", "drones", String.valueOf(dronesMade)); System.out.println("[Counter] Drones assembled: " + this.dronesMade); System.out.println("--------------------------------------------------------------"); //Clear components and set table to empty this.components[0] = null; this.components[1] = null; tableFull = false; + logger.logEventKV(Thread.currentThread().getName(), "TABLE_EMPTY", "drones", String.valueOf(dronesMade)); // Random delay to simulate real scenario try { + logger.logEvent(Thread.currentThread().getName(), "WORK_START"); Thread.sleep((int)(Math.random() * 1000)); + logger.logEvent(Thread.currentThread().getName(), "WORK_END"); } catch (InterruptedException e) {} - notifyAll(); //Notify Technicians and Agent that components have changed + logger.logEvent(Thread.currentThread().getName(), "READY"); + notifyAll(); //Notify Technicians and Agent that components have changed } /** - * Method used to check if the component given is one of the two components on the table + * Method used to check if the component given is one of the two components on the table. * - * @param components Component from Technician (used to check if Technician can accept the components on the table) - * @return True if component is on the table, false otherwise + * @param techOwns Component from Technician (to check if Technician can accept the components on the table) + * @return True if component is on the table, false otherwise. */ - private boolean componentsContains (Components components){ - //If there are no components on the table, or one of the components on the table is the same as the component given from the Technician, return True; false otherwise - return (this.components[0] == null || this.components[1] == null || (this.components[0] == components || this.components[1] == components)); + private boolean canTake(Components techOwns) { + if (!tableFull) return false; + if (this.components[0] == null || this.components[1] == null) return false; + return (this.components[0] != techOwns && this.components[1] != techOwns); } + /** * Getter method for getDronesMade. * @@ -109,6 +147,13 @@ public int getDronesAssembled(){ return this.dronesMade; } + /** + * Returns the maximum number of drones needed for the job to complete. + * + * @return The maximum number of drones for job completion. + */ + public int getMaxDrones() { return MAX_DRONES; } + /** * Method used to create new Technician threads; keeps consistency of naming conventions between threads * @@ -125,21 +170,66 @@ private static Thread makeNewTechnician(AssemblyTable t, Components i){ * * @param args */ - public static void main (String[] args){ + public static void main(String[] args) { + // Threads for each Technician and the Agent + Thread techFrame, techProp, techCtrl, agent; + + // Common table for all threads + AssemblyTable assemblyTable = new AssemblyTable(); + + // Start the logger (daemon flusher inside) + EventLogger logger = EventLogger.getInstance(); + + + // Create file with standardized naming + String runId = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); + String logFile = "assembly_log_" + runId + ".txt"; + logger.setLogFileName(logFile); + logger.setFlushIntervalMs(250L); - Thread TechnicianFrame, TechnicianPropulsion, TechnicianControl, agent; //Threads for each Technician and the Agent - AssemblyTable assemblyTable; //Table + logger.start(); // background daemon flusher + // System-wide lifecycle anchors for analyzer + logger.logEventKV("AssemblyLine", "SYSTEM_START", "drones", "0"); + logger.logEventKV("AssemblyLine", "ASSEMBLY_TABLE_READY", "drones", "0"); - assemblyTable = new AssemblyTable(); //Common Table for all Technicians and Agent - agent = new Thread(new Agent(assemblyTable), "Agent"); //Agent thread created - TechnicianFrame = makeNewTechnician(assemblyTable, Components.Frame); //Beans Technician created - TechnicianPropulsion = makeNewTechnician(assemblyTable, Components.PropulsionUnit); //Water Technician created - TechnicianControl = makeNewTechnician(assemblyTable, Components.ControlFirmware); //Sugar Technician created + // Create threads with clear names (you already do this in makeNewTechnician) + agent = new Thread(new Agent(assemblyTable), "Agent"); + techFrame = makeNewTechnician(assemblyTable, Components.Frame); + techProp = makeNewTechnician(assemblyTable, Components.PropulsionUnit); + techCtrl = makeNewTechnician(assemblyTable, Components.ControlFirmware); - //Start all Technician and Agent threads - TechnicianFrame.start(); - TechnicianPropulsion.start(); - TechnicianControl.start(); + // Check-in logs before starting threads + logger.logEventKV("AssemblyLine", "FRAME_TECH_CHECKIN", "drones", "0"); + techFrame.start(); + logger.logEventKV("AssemblyLine", "PROPULSION_TECH_CHECKIN", "drones", "0"); + techProp.start(); + logger.logEventKV("AssemblyLine", "CONTROL_TECH_CHECKIN", "drones", "0"); + techCtrl.start(); + logger.logEventKV("AssemblyLine", "AGENT_CHECKIN", "drones", "0"); agent.start(); + + // Wait for completion + try { + agent.join(); + techFrame.join(); + techProp.join(); + techCtrl.join(); + } catch (InterruptedException e) { e.printStackTrace(); } + + // Completion summary and SYSTEM_END + logger.logEventKV("AssemblyLine", "JOB_COMPLETED", "drones", String.valueOf(assemblyTable.getDronesAssembled())); + logger.logEventKV("AssemblyLine", "SYSTEM_END", "drones", String.valueOf(assemblyTable.getDronesAssembled())); + + // Stop logger and flush remaining records + logger.stop(); + System.out.println("All threads finished; system terminated."); + + + // Run the analyzer AFTER logs are fully flushed (prints to console AND writes metrics.txt) + try { + LogAnalyzer.main(new String[]{ logFile }); + System.out.println("Metrics written to metrics.txt"); + } catch (Exception e) { e.printStackTrace(); } } } diff --git a/assignment04/Components.java b/assignment04/Components.java index 1dbe3a9..e9f979c 100644 --- a/assignment04/Components.java +++ b/assignment04/Components.java @@ -1,3 +1,4 @@ +import java.util.concurrent.ThreadLocalRandom; // Thread - safe /** * Components enums * @@ -6,10 +7,10 @@ * Carleton University * @version 1.0, January 07th, 2025 * @version 2.0, January 10th, 2026 + * + * @author Lavji, F + * @version 2.1, March 14, 2026 */ - -import java.util.Random; - public enum Components { Frame, PropulsionUnit, @@ -17,10 +18,12 @@ public enum Components { /** * Pick a random value of the Components enum. + * * @return a random Component. */ public static Components getRandomComponent() { - Random random = new Random(); - return values()[random.nextInt(values().length)]; + // Switched out with thread-safe Randomizer + int i = ThreadLocalRandom.current().nextInt(values().length); + return values()[i]; } } diff --git a/assignment04/EventLogger.java b/assignment04/EventLogger.java new file mode 100644 index 0000000..4c27271 --- /dev/null +++ b/assignment04/EventLogger.java @@ -0,0 +1,192 @@ +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; +/** + * Central event logger for the assembly system. + * Logs are buffered in memory and periodically flushed + * to disk by a background daemon thread. + * + * @author Lavji, F + * @version 1.0, March.14.2026 + */ +public class EventLogger { + + // CONSTANTS + private static final String DEFAULT_LOG_FILE_NAME = "assembly_log.txt"; + private volatile String logFileName = DEFAULT_LOG_FILE_NAME; + private static final long DEFAULT_FLUSH_INTERVAL_MS = 1000L; // 1 second + private volatile long flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS; + + + private static final EventLogger INSTANCE = new EventLogger(); + + private final List buffer = new ArrayList<>(); + private static final DateTimeFormatter TS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + private volatile boolean running = false; + private Thread flusherThread; + + /** + * Singleton constructor. + */ + private EventLogger() { } + + /** + * Returns the singleton instance. + * @return Instance of EventLogger class object. + */ + public static EventLogger getInstance() { return INSTANCE; } + + /** + * Sets the flush interval in milliseconds for fine tuning. + * + * @param intervalMs Sets the flush interval in milliseconds. + */ + + public synchronized void setFlushIntervalMs(long intervalMs) { + // do not change while running + if (running) return; + if (intervalMs > 0) { flushIntervalMs = intervalMs; } + } + + /** + * Sets the log file name. + * + * @param fileName The name of the log file. + */ + public synchronized void setLogFileName(String fileName) { + if (running) return; // don’t change while running + if (fileName != null && !fileName.trim().isEmpty()) { this.logFileName = fileName.trim(); } + // Fallback + else { this.logFileName = DEFAULT_LOG_FILE_NAME; } + } + + /** + * Starts the background daemon flusher thread. + * Should be called once at program startup. + */ + public synchronized void start() { + if (running) { return; } + running = true; + flusherThread = new Thread(() -> { + while (running) { + try { Thread.sleep(flushIntervalMs); } + catch (InterruptedException e) { // ignore interruptions; loop checks running flags + } flushToDisk(); + } // Final flush after loop exits. + flushToDisk(); }, "EventLogger-Flusher"); + flusherThread.setDaemon(true); // set to daemon thread + flusherThread.start(); + } + + /** + * Stops the logger and forces a final flush. + * To be called near program termination. + */ + public synchronized void stop() { + running = false; + if (flusherThread != null) { + flusherThread.interrupt(); + try { flusherThread.join(500); } catch (InterruptedException ignored) {} + } flushToDisk(); + } + + /** + * Generic log formatter. + * Event Log: [TIME, ENTITY, EVENT_CODE, additionalData] + * + * @param entity The thread or resource that generated the event. + * @param eventCode A short identifier describing the event. + * @param additionalData Extraneous data for calculation of metrics, omitted if null. + */ + public void logEvent(String entity, String eventCode, String additionalData) { + String time = LocalDateTime.now().format(TS_FMT); + StringBuilder sb = new StringBuilder(); + sb.append("Event log: [").append(time).append(" , ").append(entity).append(" , ").append(eventCode); + if (additionalData != null && !additionalData.isEmpty()) { sb.append(" , ").append(additionalData); } + sb.append("]"); + synchronized (buffer) { buffer.add(sb.toString()); } + } + + /** + * Sanity logger for redundancy. + * + * @param entity The thread or resource that generated the event. + * @param eventCode A short identifier describing the event. + */ + public void logEvent(String entity, String eventCode) { logEvent(entity, eventCode, ""); } + + /** + * Flushes current buffer to file and clears the in-memory buffer. + */ + private void flushToDisk() { + List toWrite; + synchronized (buffer) { + if (buffer.isEmpty()) { return; } + toWrite = new ArrayList<>(buffer); + buffer.clear(); + } + try (FileWriter fw = new FileWriter(logFileName, true)) { + for (String line : toWrite) { + fw.write(line); + fw.write(System.lineSeparator()); + } + } catch (IOException e) { e.printStackTrace(); } + } + + /** + * Build a semicolon-separated key=value string from a map, preserving insertion order. + * Values are written as-is enabling logging "[Frame,ControlFirmware]" for components. + */ + private static String toAdditionalData(Map kv) { + if (kv == null || kv.isEmpty()) return ""; + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Map.Entry e : kv.entrySet()) { + if (!first) sb.append("; "); + sb.append(e.getKey()).append("=").append(e.getValue()); + first = false; + } return sb.toString(); + } + + /** + * Convenience: supply additional data as varargs: ("k1","v1","k2","v2",...) + * Throws IllegalArgumentException if odd number of tokens is passed. + */ + public void logEventKV(String entity, String eventCode, String... kvPairs) { + if (kvPairs == null || kvPairs.length == 0) { + logEvent(entity, eventCode, ""); + return; + } + + if (kvPairs.length % 2 != 0) { throw new IllegalArgumentException("kvPairs must be even-length: key,value,..."); } + Map kv = new LinkedHashMap<>(); + for (int i = 0; i < kvPairs.length; i += 2) { + String k = kvPairs[i] == null ? "" : kvPairs[i].trim(); + String v = kvPairs[i + 1] == null ? "" : kvPairs[i + 1].trim(); + if (!k.isEmpty()) kv.put(k, v); + } logEvent(entity, eventCode, toAdditionalData(kv)); + } + + /** + * Convenience: supply additional data as a map (insertion order respected). + */ + public void logEvent(String entity, String eventCode, Map kv) { logEvent(entity, eventCode, toAdditionalData(kv)); } + + /** + * Wait markers so all threads/classes use the same wording. + * + * Emits WAIT_START (no state field forced). + */ + public void waitStart(String entity) { logEvent(entity, "WAIT_START", ""); } + + /** + * Wait end marker. + */ + public void waitEnd(String entity) { logEvent(entity, "WAIT_END", ""); } +} \ No newline at end of file diff --git a/assignment04/LogAnalyzer.java b/assignment04/LogAnalyzer.java new file mode 100644 index 0000000..dd14dfe --- /dev/null +++ b/assignment04/LogAnalyzer.java @@ -0,0 +1,179 @@ +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +/** + * Standalone analyzer for assembly_log.txt. + * Runs AFTER the assembly run has finished. + * + * @author Lavji, F + * @version 1.1, Mar 14, 2026 + */ +public class LogAnalyzer { + + private static final String LOG_FILE_NAME = "assembly_log.txt"; + private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + + static class ThreadStats { + long firstTime = Long.MAX_VALUE; + long lastTime = Long.MIN_VALUE; + long totalWaiting = 0L; + Long lastWaitingStart = null; + long totalBusy = 0L; + Long lastBusyStart = null; + List responseTimes = new ArrayList<>(); + } + + public static void main(String[] args) throws IOException, ParseException { + String logFile = (args != null && args.length > 0 + && args[0] != null && !args[0].trim().isEmpty()) ? args[0].trim() : LOG_FILE_NAME; + + Map statsMap = new HashMap<>(); + long systemStart = Long.MAX_VALUE; + long systemEnd = Long.MIN_VALUE; + int totalDrones = 0; + StringBuilder out = new StringBuilder(); // accumulate all output lines + + try (BufferedReader br = new BufferedReader(new FileReader(logFile))) { + String line; + while ((line = br.readLine()) != null) { + int l = line.indexOf('['), r = line.lastIndexOf(']'); + if (l < 0 || r <= l) continue; + String content = line.substring(l + 1, r); + + String[] parts = content.split(",", 4); + if (parts.length < 3) continue; + for (int i = 0; i < parts.length; i++) parts[i] = parts[i].trim(); + + String timeStr = parts[0]; + String entity = parts[1]; + String event = parts[2]; + String extra = (parts.length == 4) ? parts[3] : ""; + long timeMs = formatter.parse(timeStr).getTime(); + + // window (also respects SYSTEM_START/END anchors) + systemStart = Math.min(systemStart, timeMs); + systemEnd = Math.max(systemEnd, timeMs); + if (entity.equals("AssemblyLine")) { + if (event.equals("SYSTEM_START")) systemStart = Math.min(systemStart, timeMs); + else if (event.equals("SYSTEM_END")) systemEnd = Math.max(systemEnd, timeMs); + } + + Map kv = new HashMap<>(); + if (!extra.isEmpty()) { + for (String token : extra.split(";")) { + token = token.trim(); + int eq = token.indexOf('='); + if (eq > 0 && eq < token.length() - 1) { + String k = token.substring(0, eq).trim(); + String v = token.substring(eq + 1).trim(); + kv.put(k, v); + } + } + } + + if (event.equals("DRONE_ASSEMBLED")) { totalDrones++; } + + if (entity.startsWith("Agent") || entity.startsWith("Technician")) { + ThreadStats ts = statsMap.computeIfAbsent(entity, k -> new ThreadStats()); + ts.firstTime = Math.min(ts.firstTime, timeMs); + ts.lastTime = Math.max(ts.lastTime, timeMs); + + if (event.equals("WAIT_START")) { ts.lastWaitingStart = timeMs; } + else if (event.equals("WAIT_END") && ts.lastWaitingStart != null) { + ts.totalWaiting += (timeMs - ts.lastWaitingStart); + ts.lastWaitingStart = null; + } + + String ev = event.toUpperCase(java.util.Locale.ROOT); + if (ev.equals("RESPONSE_TIME_MS") || ev.equals("RESPONSE_TIME")) { + String dur = kv.get("duration"); + if (dur == null && extra.contains("=")) { + String[] kv2 = extra.split("="); + if (kv2.length == 2) dur = kv2[1].trim(); + } + if (dur != null) { + try { ts.responseTimes.add(Long.parseLong(dur.replaceAll("[^0-9]", ""))); } + catch (NumberFormatException ignored) {} + } + } + } + } + } + + // close any open waits at EOF + for (ThreadStats ts : statsMap.values()) { + if (ts.lastWaitingStart != null) { + long end = (systemEnd > 0 && systemEnd != Long.MIN_VALUE) ? systemEnd : ts.lastTime; + if (end > ts.lastWaitingStart) ts.totalWaiting += (end - ts.lastWaitingStart); + ts.lastWaitingStart = null; + } + } + + out.append("===== METRICS ANALYSIS =====\n"); + out.append(String.format("Log file: %s%n", logFile)); // helpful context + + // Empty-log handling (no timestamps or no drones) + boolean hasWindow = (systemStart < systemEnd); + if (!hasWindow) { + out.append("No valid timestamps found in the log. Is the file empty?\n"); + } else { + double durationSec = (systemEnd - systemStart) / 1000.0; + + if (totalDrones == 0) { + out.append("No assemblies detected (0 DRONE_ASSEMBLED events).\n"); + out.append(String.format("Total time observed: %.3f s%n", durationSec)); + } else { + double throughput = (durationSec > 0) ? (totalDrones / durationSec) : 0.0; + out.append(String.format("Total drones assembled: %d%n", totalDrones)); + out.append(String.format("Total time: %.3f s%n", durationSec)); + out.append(String.format("Throughput: %.3f drones/s%n", throughput)); + } + } + + out.append("\n"); + out.append("Per-thread metrics:\n"); + if (statsMap.isEmpty()) { + out.append("(No Agent/Technician events found.)\n"); + } else { + for (Map.Entry entry : statsMap.entrySet()) { + String threadName = entry.getKey(); + ThreadStats ts = entry.getValue(); + + long totalTime = Math.max(0L, ts.lastTime - ts.firstTime); + long waiting = Math.min(ts.totalWaiting, totalTime); + long busyTime = Math.max(0L, totalTime - waiting); + double utilization = (totalTime > 0) ? (busyTime / (double) totalTime) : 0.0; + + double avgResp = 0.0; + if (!ts.responseTimes.isEmpty()) { + long sum = 0; + for (Long rt : ts.responseTimes) sum += rt; + avgResp = sum / (double) ts.responseTimes.size(); + } + + out.append("-------------------------------------\n"); + out.append("Thread: ").append(threadName).append('\n'); + out.append(String.format("Total time: %.3f s%n", totalTime / 1000.0)); + out.append(String.format("Total waiting: %.3f s%n", waiting / 1000.0)); + out.append(String.format("Utilization (busy/total): %.3f%n", utilization)); + out.append(String.format("Average response time: %.3f ms (over %d samples)%n", + avgResp, ts.responseTimes.size())); + } + } + + // Print to console + System.out.print(out.toString()); + + // Also write the exact same output to metrics.txt + try (java.io.PrintWriter pw = new java.io.PrintWriter( + new java.io.OutputStreamWriter( + new java.io.FileOutputStream("metrics.txt", false), + java.nio.charset.StandardCharsets.UTF_8))) { + pw.write(out.toString()); + pw.flush(); + } + } +} \ No newline at end of file diff --git a/assignment04/Technician.java b/assignment04/Technician.java index 7f4eec1..b03f277 100644 --- a/assignment04/Technician.java +++ b/assignment04/Technician.java @@ -9,11 +9,17 @@ * Carleton University * @version 1.0, January 07th, 2025 * @version 2.0, January 10th, 2026 + * + * @author Lavji, F + * @version 2.1, March 14, 2026 */ public class Technician implements Runnable { - private AssemblyTable assemblyTable; //The common table between Agent and Technicians - private Components components; //The only component each instance of Technician has an infinite supply of (this component is different between all three Technicians) + private AssemblyTable assemblyTable; //The common table between Agent and Technicians + private Components components; //The only component each instance of Technician has an infinite supply of (this component is different between all three Technicians) + + // NEW --> Addition of Event Logger (Assignment04_Requirement01) + private final EventLogger logger = EventLogger.getInstance(); /** * Constructor for Technician @@ -27,19 +33,40 @@ public Technician(AssemblyTable t, Components i){ } /** - * Method used for each Technician thread when ran + * Method used for each Technician thread when ran. */ public void run(){ + String name = Thread.currentThread().getName(); + logger.logEvent(name, "THREAD_START", "Technician with infinite " + components); + System.out.println("[" + Thread.currentThread().getName() + "] Waiting for remaining components..."); - while (this.assemblyTable.getDronesAssembled() != 20){ //Will loop until 20 drones have been assembled - this.assemblyTable.getComponents(this.components); //Attempts to obtain the missing components for the Technician (if obtained, drone is assembled) - // Sleep for a random time between 0 and 5 seconds to simulate assembly time + + // Loop until MAX_DRONES have been assembled + while (this.assemblyTable.getDronesAssembled() != this.assemblyTable.getMaxDrones()) { + + long startAttempt = System.currentTimeMillis(); + + //Attempts to obtain the missing components for the Technician (if obtained, drone is assembled) + logger.logEvent(name, "RETRIEVING_COMPONENT"); + this.assemblyTable.getComponents(this.components); + + // Sleep for between 0 and 5 seconds before calculating n! try { + logger.logEvent(name, "WORK_START"); Thread.sleep((int)(Math.random() * 5000)); + logger.logEvent(name, "WORK_END"); } catch (InterruptedException e) {} + + // NEW --> Metric Analysis (Assignment04_Requirement03) + long endAttempt = System.currentTimeMillis(); + long responseTime = endAttempt - startAttempt; + logger.logEventKV(name, "RESPONSE_TIME", "duration", String.valueOf(responseTime)); } //All drones have been assembled - System.out.println("[" + Thread.currentThread().getName() + "] 20 drones assembled, ending..."); + logger.logEvent(name, "THREAD_END", + String.format("%d drones assembled, ending...", this.assemblyTable.getMaxDrones())); + System.out.println(String.format("[%s] %d drones assembled, ending...", + Thread.currentThread().getName(), this.assemblyTable.getMaxDrones())); } } diff --git a/assignment04/documentation/README.html b/assignment04/documentation/README.html new file mode 100644 index 0000000..c9b4fd4 --- /dev/null +++ b/assignment04/documentation/README.html @@ -0,0 +1,89 @@ + + + + +Drone Assembly Line — Logging & Metrics (Extended) + + + +

DRONE ASSEMBLY LINE — LOGGING & METRICS SYSTEM

+
SYSC 3303A W2026 • Dr. Rami Sabouni • Assignment 4
+
+ +

1) Overview

+

Agent places two components; each Technician owns one component and assembles a drone with the other two. AssemblyTable (monitor) synchronizes wait()/notifyAll(). EventLogger buffers events and flushes via a daemon thread to a run‑ID log. LogAnalyzer computes throughput, utilization, and per‑thread response times from the log.

+ +
+
+

Core Events

+
    +
  • WAIT_START/WAIT_END — emitted at real monitor waits.
  • +
  • PLACED_COMPONENTS, PICKED_UP, DRONE_ASSEMBLED.
  • +
  • TABLE_FULL, TABLE_EMPTY.
  • +
  • WORK_START/WORK_END, RESPONSE_TIME.
  • +
  • SYSTEM_START/SYSTEM_END, JOB_COMPLETED.
  • +
+
+
+

Log Line Format

+
Event log: [yyyy-MM-dd HH:mm:ss.SSS , ENTITY , EVENT_CODE , key=value; ...]
+
Keys typically include components=[...,...], drones, and duration.
+
+
+ +
+ +

2) Build & Run

+
    +
  1. Open in IntelliJ → ensure SDK is Java 8+.
  2. +
  3. Run AssemblyTable.main().
  4. +
  5. At shutdown, LogAnalyzer.main() runs automatically and writes metrics.txt.
  6. +
+ +

Config

+
    +
  • Max drones: AssemblyTable.getMaxDrones()
  • +
  • Log file: EventLogger.setLogFileName(...)
  • +
  • Flush interval: EventLogger.setFlushIntervalMs(...)
  • +
+ +
+ +

3) Diagrams

+
+
+ UML Class Diagram +
Figure 1 — UML Class Diagram (Agent, Technician, AssemblyTable monitor, EventLogger, AssemblyLine)
+
+
+ UML Sequence Diagram (Loop, Colour) +
Figure 2 — Sequence Diagram (loop until MAX_DRONES)
+
+
+ UML Sequence Diagram (Simple) +
Appendix — Sequence Diagram (single cycle)
+
+
+ +
+ +

4) Output

+
    +
  • assembly_log_YYYYMMDD_HHMMSS.txt — run‑ID log
  • +
  • metrics.txt — analyzer summary (throughput, utilization, response)
  • +
+ + + \ No newline at end of file diff --git a/assignment04/documentation/README.md b/assignment04/documentation/README.md new file mode 100644 index 0000000..e87ad89 --- /dev/null +++ b/assignment04/documentation/README.md @@ -0,0 +1,574 @@ +# DRONE ASSEMBLY LINE — LOGGING & METRICS SYSTEM +_SYSC3303A • RTConcurrentSys • WINTER2026 • Assignment04_ + +## 1. Simplified Deliverable Requirements +The system extends the classic **Agent–Technician–Monitor** drone‑assembly concurrency problem by adding: +- A **daemonized EventLogger** (buffered, asynchronous) +- A **run‑ID log file** per execution (`assembly_log_YYYYMMDD_HHMMSS.txt`) +- A **concurrency‑safe measurement layer** that captures: + - Wait times (`WAIT_START`, `WAIT_END`) + - Work durations (`WORK_START`, `WORK_END`) + - Response times (`RESPONSE_TIME`) + - Throughput + utilization +- A fully automated **LogAnalyzer** that computes: + - Total drones assembled + - Total run time + - Drones per second + - Per‑thread waiting time and utilization + - Average response time + +### 1.1. Agent (Producer Thread) +- Randomly selects **two distinct components** +- Requests placement onto `AssemblyTable` +- Logs: + - `COMPONENTS_SELECTED` + - `PLACED_COMPONENTS` + - `COMPONENTS_ADDED` + - `WORK_START / WORK_END` + - `RESPONSE_TIME` + - `THREAD_START / THREAD_END` + +### 1.2. Technician (Consumer Thread) +- Each Technician has **infinite supply** of exactly **one** component +- Waits until the *other two* appear on the table +- Logs: + - `RETRIEVING_COMPONENT` + - `PICKED_UP` + - `DRONE_ASSEMBLED` + - `WORK_START / WORK_END` + - `RESPONSE_TIME` + - `THREAD_START / THREAD_END` + +### 1.3. AssemblyTable (Monitor) +- Controls synchronized access to: + - `addComponents(...)` (Agent) + - `getComponents(...)` (Technicians) +- Emits **true wait markers** exactly at blocking points: + - `WAIT_START` + - `WAIT_END` +- Logs: + - `TABLE_FULL` + - `TABLE_EMPTY` + - `READY` (state change notifications) + - `SYSTEM_START` + - `SYSTEM_END` + - `JOB_COMPLETED` + +### 1.4. EventLogger (Utility + Daemon) +- Singleton, thread‑safe, buffered +- Runs a **background flusher thread** +- Configurable via: + - `setLogFileName(...)` + - `setFlushIntervalMs(...)` +- Writes all log entries to a **run‑ID file** + +### 1.5. LogAnalyzer (Standalone Parser) +- Runs automatically after the system ends +- Reads the generated run‑ID log +- Produces a detailed human‑readable `metrics.txt` + +*** +## 2. File Structure +### 2.1. Recommended Layout +# DRONE ASSEMBLY LINE — LOGGING & METRICS SYSTEM +_SYSC3303A • RTConcurrentSys • WINTER2026 • Assignment04_ + +## 1. Simplified Deliverable Requirements +The system extends the classic **Agent–Technician–Monitor** drone‑assembly concurrency problem by adding: +- A **daemonized EventLogger** (buffered, asynchronous) +- A **run‑ID log file** per execution (`assembly_log_YYYYMMDD_HHMMSS.txt`) +- A **concurrency‑safe measurement layer** that captures: + - Wait times (`WAIT_START`, `WAIT_END`) + - Work durations (`WORK_START`, `WORK_END`) + - Response times (`RESPONSE_TIME`) + - Throughput + utilization +- A fully automated **LogAnalyzer** that computes: + - Total drones assembled + - Total run time + - Drones per second + - Per‑thread waiting time and utilization + - Average response time + +### 1.1. Agent (Producer Thread) +- Randomly selects **two distinct components** +- Requests placement onto `AssemblyTable` +- Logs: + - `COMPONENTS_SELECTED` + - `PLACED_COMPONENTS` + - `COMPONENTS_ADDED` + - `WORK_START / WORK_END` + - `RESPONSE_TIME` + - `THREAD_START / THREAD_END` + +### 1.2. Technician (Consumer Thread) +- Each Technician has **infinite supply** of exactly **one** component +- Waits until the *other two* appear on the table +- Logs: + - `RETRIEVING_COMPONENT` + - `PICKED_UP` + - `DRONE_ASSEMBLED` + - `WORK_START / WORK_END` + - `RESPONSE_TIME` + - `THREAD_START / THREAD_END` + +### 1.3. AssemblyTable (Monitor) +- Controls synchronized access to: + - `addComponents(...)` (Agent) + - `getComponents(...)` (Technicians) +- Emits **true wait markers** exactly at blocking points: + - `WAIT_START` + - `WAIT_END` +- Logs: + - `TABLE_FULL` + - `TABLE_EMPTY` + - `READY` (state change notifications) + - `SYSTEM_START` + - `SYSTEM_END` + - `JOB_COMPLETED` + +### 1.4. EventLogger (Utility + Daemon) +- Singleton, thread‑safe, buffered +- Runs a **background flusher thread** +- Configurable via: + - `setLogFileName(...)` + - `setFlushIntervalMs(...)` +- Writes all log entries to a **run‑ID file** + +### 1.5. LogAnalyzer (Standalone Parser) +- Runs automatically after the system ends +- Reads the generated run‑ID log +- Produces a detailed human‑readable `metrics.txt` + +*** +## 2. File Structure +### 2.1. Recommended Layout + assignment04/ + ├── Agent.java + ├── AssemblyTable.java + ├── Components.java + ├── EventLogger.java + ├── LogAnalyzer.java + ├── Technician.java + │ + ├── umlDiagrams/ + │ ├── UML_Class_Diagram.png + │ ├── UML_Sequence_Diagram_No_Loop.png + │ ├── UML_Sequence_Diagram_With_Loop.png + │ + ├── documentation/ + │ ├── README.html + │ ├── README_extended.html + │ ├── README_concise.html + │ ├── README.md + │ ├── README.txt + │ + ├── metrics.txt # auto-generated after run + └── assembly_log_*.txt # run‑ID logs generated per execution +### 2.2. Primary Executables +Run in this order: +1. `AssemblyTable.main()` +- Spawns Agent + 3 Technicians +- Starts EventLogger +- Orchestrates the entire system +2. `LogAnalyzer.main()` +- **Automatically called** at the end of execution +- Generates metrics summary + `metrics.txt` + +*** +## 3. How to Run (IntelliJ) +1. Open project → ensure SDK = **Java 8+** +2. Run **`AssemblyTable.main()`** +3. Wait until: + - All threads finish + - Logger flushes and shuts down + - `LogAnalyzer` executes +4. Open produced files: + - `assembly_log_YYYYMMDD_HHMMSS.txt` + - `metrics.txt` + +*** +## 4. Diagrams +## 4.1. UML Class Diagram +### 4.1.1 Drone Assembly (Concurrency + Logging) +```mermaid +classDiagram + direction LR + + class Runnable { + <> + + run() + } + + class EventLogger { + <> + - logFileName : String + - flushIntervalMs : long + - buffer : List~String~ + + getInstance() + + start() + + stop() + + setLogFileName(name) + + setFlushIntervalMs(ms) + + logEvent(e, evt, data) + + logEventKV(e, evt, kv...) + + waitStart(e) + + waitEnd(e) + } + + class AssemblyTable { + <> + - components : Components[2] + - tableFull : boolean + - dronesMade : int + + addComponents(c1, c2) + + getComponents(owns) + + getDronesAssembled() + + getMaxDrones() + } + + class Agent { + - assemblyTable : AssemblyTable + - logger : EventLogger + + run() + } + + class Technician { + - assemblyTable : AssemblyTable + - components : Components + - logger : EventLogger + + run() + } + + class AssemblyLine { + + main(args) + } + + class Components { + <> + Frame + PropulsionUnit + ControlFirmware + + getRandomComponent() + } + + Agent ..|> Runnable + Technician ..|> Runnable + + Agent --> AssemblyTable : uses + Technician --> AssemblyTable : uses + AssemblyLine --> Agent : creates/starts + AssemblyLine --> Technician : creates/starts + + Agent ..> EventLogger : calls logEvent(...) + Technician ..> EventLogger : calls logEvent(...) + AssemblyTable ..> EventLogger : calls logEvent(...) +``` +## 4.2. Sequence Diagrams +### 4.2.1 Single Assembly Cycle (No Loop) +```mermaid + sequenceDiagram + autonumber + participant A as Agent + participant T as Technician + participant M as AssemblyTable + participant L as EventLogger + + A->>M: addComponents(c1, c2) + M-->>L: logEvent(PLACED_COMPONENTS) + M-->>L: logEvent(TABLE_FULL) + M-->>T: notifyAll() + + T->>M: getComponents(owns) + M-->>L: logEvent(PICKED_UP) + M-->>L: logEvent(DRONE_ASSEMBLED) + M-->>L: logEvent(TABLE_EMPTY) + M-->>A: notifyAll() + + A->>L: WORK_START / WORK_END + T->>L: WORK_START / WORK_END +``` +### 4.2.2 One Assembly Cycle (Looped Notation) +```mermaid +sequenceDiagram + autonumber + participant A as Agent + participant T as Technician + participant M as AssemblyTable + participant L as EventLogger + + loop until dronesMade == MAX_DRONES + A->>M: addComponents(c1, c2) + M-->>L: logEvent(PLACED_COMPONENTS) + M-->>L: logEvent(TABLE_FULL) + M-->>T: notifyAll() + + T->>M: getComponents(owns) + M-->>L: logEvent(PICKED_UP) + M-->>L: logEvent(DRONE_ASSEMBLED) + M-->>L: logEvent(TABLE_EMPTY) + M-->>A: notifyAll() + + A->>L: WORK_START / WORK_END + T->>L: WORK_START / WORK_END + end +``` +*** +## 5. Logging Format +Every entry follows: + + Event log: [yyyy-MM-dd HH:mm:ss.SSS , ENTITY , EVENT_CODE , key=value; key=value ...] + +**Examples**: + + Event log: [2026-03-14 17:04:22.119 , Agent , PLACED_COMPONENTS , components=[Frame,ControlFirmware]; drones=11] + Event log: [2026-03-14 17:04:22.892 , Technician-Frame , DRONE_ASSEMBLED , drones=12] + +*** +## 6. Metrics Output (metrics.txt) +Generated automatically by `LogAnalyzer`: +- Total drones assembled +- Total time (s) +- Throughput (drones/s) +- For **each** Agent/Technician: + - Total active time + - Waiting time + - Utilization = busy / total + - Average response time (ms) + +Example excerpt: + + ===== METRICS ANALYSIS ===== + Total drones assembled: 20 + Total time: 8.542 s + Throughput: 2.342 drones/s + + Per-thread metrics: + ------------------------------------- + Thread: Agent + Total time: 8.541 s + Total waiting: 1.222 s + Utilization: 0.856 + Average response time: 421.5 ms + +*** +## 7. Integration Notes +- **Wait markers** generated only where real `wait()` occurs → accurate utilization. +- **Simulated work** wrapped in `WORK_START / WORK_END`. +- **logEventKV** used for structured metadata (`duration`, `components`, `drones`). +- **daemon thread** ensures flusher does not block application exit. +- **run‑ID log filenames** avoids overwriting previous data. + +*** +## 8. Submission Artifacts +Include the following: +- Source files (`.java`) +- `documentation\README.txt` +- `documentation\README.md` +- `documentation\README.pdf` +- `umlDiagrams\UML_Sequence_Diagram_With_Loop.png` +- `umlDiagrams\UML_Sequence_Diagram_No_Loop.png` +- `umlDiagrams\UML_Class_Diagram.png` +### 2.2. Primary Executables +Run in this order: +1. `AssemblyTable.main()` + - Spawns Agent + 3 Technicians + - Starts EventLogger + - Orchestrates the entire system +2. `LogAnalyzer.main()` + - **Automatically called** at the end of execution + - Generates metrics summary + `metrics.txt` + +*** +## 3. How to Run (IntelliJ) +1. Open project → ensure SDK = **Java 8+** +2. Run **`AssemblyTable.main()`** +3. Wait until: + - All threads finish + - Logger flushes and shuts down + - `LogAnalyzer` executes +4. Open produced files: + - `assembly_log_YYYYMMDD_HHMMSS.txt` + - `metrics.txt` + +*** +## 4. Diagrams +## 4.1. UML Class Diagram +### 4.1.1 Drone Assembly (Concurrency + Logging) +```mermaid +classDiagram + direction LR + + class Runnable { + <> + + run() + } + + class EventLogger { + <> + - logFileName : String + - flushIntervalMs : long + - buffer : List~String~ + + getInstance() + + start() + + stop() + + setLogFileName(name) + + setFlushIntervalMs(ms) + + logEvent(e, evt, data) + + logEventKV(e, evt, kv...) + + waitStart(e) + + waitEnd(e) + } + + class AssemblyTable { + <> + - components : Components[2] + - tableFull : boolean + - dronesMade : int + + addComponents(c1, c2) + + getComponents(owns) + + getDronesAssembled() + + getMaxDrones() + } + + class Agent { + - assemblyTable : AssemblyTable + - logger : EventLogger + + run() + } + + class Technician { + - assemblyTable : AssemblyTable + - components : Components + - logger : EventLogger + + run() + } + + class AssemblyLine { + + main(args) + } + + class Components { + <> + Frame + PropulsionUnit + ControlFirmware + + getRandomComponent() + } + + Agent ..|> Runnable + Technician ..|> Runnable + + Agent --> AssemblyTable : uses + Technician --> AssemblyTable : uses + AssemblyLine --> Agent : creates/starts + AssemblyLine --> Technician : creates/starts + + Agent ..> EventLogger : calls logEvent(...) + Technician ..> EventLogger : calls logEvent(...) + AssemblyTable ..> EventLogger : calls logEvent(...) +``` +## 4.2. Sequence Diagrams +### 4.2.1 Single Assembly Cycle (No Loop) +```mermaid + sequenceDiagram + autonumber + participant A as Agent + participant T as Technician + participant M as AssemblyTable + participant L as EventLogger + + A->>M: addComponents(c1, c2) + M-->>L: logEvent(PLACED_COMPONENTS) + M-->>L: logEvent(TABLE_FULL) + M-->>T: notifyAll() + + T->>M: getComponents(owns) + M-->>L: logEvent(PICKED_UP) + M-->>L: logEvent(DRONE_ASSEMBLED) + M-->>L: logEvent(TABLE_EMPTY) + M-->>A: notifyAll() + + A->>L: WORK_START / WORK_END + T->>L: WORK_START / WORK_END +``` +### 4.2.2 One Assembly Cycle (Looped Notation) +```mermaid +sequenceDiagram + autonumber + participant A as Agent + participant T as Technician + participant M as AssemblyTable + participant L as EventLogger + + loop until dronesMade == MAX_DRONES + A->>M: addComponents(c1, c2) + M-->>L: logEvent(PLACED_COMPONENTS) + M-->>L: logEvent(TABLE_FULL) + M-->>T: notifyAll() + + T->>M: getComponents(owns) + M-->>L: logEvent(PICKED_UP) + M-->>L: logEvent(DRONE_ASSEMBLED) + M-->>L: logEvent(TABLE_EMPTY) + M-->>A: notifyAll() + + A->>L: WORK_START / WORK_END + T->>L: WORK_START / WORK_END + end +``` +*** +## 5. Logging Format +Every entry follows: + + Event log: [yyyy-MM-dd HH:mm:ss.SSS , ENTITY , EVENT_CODE , key=value; key=value ...] + +**Examples**: + + Event log: [2026-03-14 17:04:22.119 , Agent , PLACED_COMPONENTS , components=[Frame,ControlFirmware]; drones=11] + Event log: [2026-03-14 17:04:22.892 , Technician-Frame , DRONE_ASSEMBLED , drones=12] + +*** +## 6. Metrics Output (metrics.txt) +Generated automatically by `LogAnalyzer`: +- Total drones assembled +- Total time (s) +- Throughput (drones/s) +- For **each** Agent/Technician: + - Total active time + - Waiting time + - Utilization = busy / total + - Average response time (ms) + +Example excerpt: + + ===== METRICS ANALYSIS ===== + Total drones assembled: 20 + Total time: 8.542 s + Throughput: 2.342 drones/s + + Per-thread metrics: + ------------------------------------- + Thread: Agent + Total time: 8.541 s + Total waiting: 1.222 s + Utilization: 0.856 + Average response time: 421.5 ms + +*** +## 7. Integration Notes +- **Wait markers** generated only where real `wait()` occurs → accurate utilization. +- **Simulated work** wrapped in `WORK_START / WORK_END`. +- **logEventKV** used for structured metadata (`duration`, `components`, `drones`). +- **daemon thread** ensures flusher does not block application exit. +- **run‑ID log filenames** avoids overwriting previous data. + +*** +## 8. Submission Artifacts +Include the following: +- Source files (`.java`) +- `documentation\README.txt` +- `documentation\README.md` +- `documentation\README.pdf` +- `umlDiagrams\MaxDrone_Looped_SequenceDiagram.png` +- `umlDiagrams\SingleRun_SequenceDiagram.png` +- `umlDiagrams\UML_Class_Diagram.png` \ No newline at end of file diff --git a/assignment04/documentation/README.pdf b/assignment04/documentation/README.pdf new file mode 100644 index 0000000..ce83fc4 Binary files /dev/null and b/assignment04/documentation/README.pdf differ diff --git a/assignment04/documentation/README.txt b/assignment04/documentation/README.txt new file mode 100644 index 0000000..f6fe44c --- /dev/null +++ b/assignment04/documentation/README.txt @@ -0,0 +1,28 @@ +DRONE ASSEMBLY LINE — LOGGING & METRICS SYSTEM +SYSC 3303A W2026 - Dr. Rami Sabouni - Assignment 4 +================================================== + +OVERVIEW +- Agent places two components; Technicians assemble drones from the other two. +- AssemblyTable is the monitor (wait/notifyAll) coordinating threads. +- EventLogger is a daemonized, buffered singleton (run-ID log file per run). +- LogAnalyzer computes throughput, utilization, and response-time metrics. + +HOW TO RUN (INTELLIJ) +1) Run AssemblyTable.main() +2) Program stops -> logger flushes -> LogAnalyzer.main() runs automatically +3) Open: assembly_log_YYYYMMDD_HHMMSS.txt and metrics.txt + +CONFIGURATION +- Max drones: AssemblyTable.getMaxDrones() +- Log file: EventLogger.setLogFileName(String) +- Flush ms: EventLogger.setFlushIntervalMs(long) + +LOG FORMAT +Event log: [yyyy-MM-dd HH:mm:ss.SSS, ENTITY, EVENT_CODE, key=value; key=value] +Core events: WAIT_START/WAIT_END, PLACED_COMPONENTS, PICKED_UP, DRONE_ASSEMBLED, +TABLE_FULL, TABLE_EMPTY, WORK_START/WORK_END, RESPONSE_TIME, SYSTEM_START/END. + +OUTPUT FILES +- assembly_log_YYYYMMDD_HHMMSS.txt (per-run log) +- metrics.txt (summary report) \ No newline at end of file diff --git a/assignment04/umlDiagrams/UML_Class_Diagram.png b/assignment04/umlDiagrams/UML_Class_Diagram.png new file mode 100644 index 0000000..21c899d Binary files /dev/null and b/assignment04/umlDiagrams/UML_Class_Diagram.png differ diff --git a/assignment04/umlDiagrams/UML_Sequence_Diagram_No_Loop.png b/assignment04/umlDiagrams/UML_Sequence_Diagram_No_Loop.png new file mode 100644 index 0000000..d8c8bdd Binary files /dev/null and b/assignment04/umlDiagrams/UML_Sequence_Diagram_No_Loop.png differ diff --git a/assignment04/umlDiagrams/UML_Sequence_Diagram_With_Loop.png b/assignment04/umlDiagrams/UML_Sequence_Diagram_With_Loop.png new file mode 100644 index 0000000..b005163 Binary files /dev/null and b/assignment04/umlDiagrams/UML_Sequence_Diagram_With_Loop.png differ