diff --git a/Theoretical Questions.pdf b/Theoretical Questions.pdf new file mode 100644 index 0000000..59ea28b Binary files /dev/null and b/Theoretical Questions.pdf differ diff --git a/Theoretical Questions.tex b/Theoretical Questions.tex new file mode 100644 index 0000000..eda8296 --- /dev/null +++ b/Theoretical Questions.tex @@ -0,0 +1,354 @@ +\documentclass[12pt,a4paper]{article} + +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage[margin=2.5cm]{geometry} +\usepackage{listings} +\usepackage{xcolor} +\usepackage{hyperref} +\usepackage{parskip} +\usepackage{booktabs} +\usepackage{array} +\usepackage{titlesec} +\usepackage{fancyhdr} +\usepackage{amsmath} +\usepackage{enumitem} + +% ── Page style ────────────────────────────────────────────────────────────── +\pagestyle{fancy} +\fancyhf{} +\rhead{Advanced Programming – Assignment 8} +\lhead{Faraz Ardeh} +\cfoot{\thepage} + +% ── Section formatting ─────────────────────────────────────────────────────── +\titleformat{\section}{\large\bfseries}{Question \thesection.}{0.6em}{} +\titleformat{\subsection}{\normalsize\bfseries}{}{0em}{} + +% ── Java code style ───────────────────────────────────────────────────────── +\definecolor{javakw}{rgb}{0.13,0.13,0.60} +\definecolor{javastr}{rgb}{0.63,0.13,0.13} +\definecolor{javacmt}{rgb}{0.38,0.38,0.38} +\definecolor{codebg}{rgb}{0.97,0.97,0.97} +\definecolor{codefr}{rgb}{0.82,0.82,0.82} + +\lstdefinestyle{java}{ + language=Java, + basicstyle=\ttfamily\small, + keywordstyle=\color{javakw}\bfseries, + stringstyle=\color{javastr}, + commentstyle=\color{javacmt}\itshape, + numberstyle=\tiny\color{gray}, + numbers=left, + stepnumber=1, + numbersep=8pt, + backgroundcolor=\color{codebg}, + frame=single, + rulecolor=\color{codefr}, + breaklines=true, + breakatwhitespace=false, + showstringspaces=false, + tabsize=4, + captionpos=b, +} + +\lstset{style=java} + +% ── Inline code ───────────────────────────────────────────────────────────── +\newcommand{\code}[1]{\texttt{\small #1}} + +% ──────────────────────────────────────────────────────────────────────────── +\begin{document} + +% ── Title page ─────────────────────────────────────────────────────────────── +\begin{titlepage} + \centering + \vspace*{3cm} + {\Huge\bfseries Multithreading Basics\par} + \vspace{0.8cm} + {\large Assignment 8 – Theoretical Questions\par} + \vspace{0.4cm} + {\large Advanced Programming (AP)\par} + \vspace{2cm} + \rule{0.6\linewidth}{0.5pt}\\[0.4cm] + {\large\bfseries Faraz Ardeh\par} + \vspace{0.3cm} + {\normalsize fa.ardeh@gmail.com\par} + \vspace{0.6cm} + {\normalsize Shahid Beheshti University\par} + \vspace{0.3cm} + {\normalsize June 2026\par} + \vfill +\end{titlepage} + +\tableofcontents +\newpage + +% ════════════════════════════════════════════════════════════════════════════ +\section{\code{start()} vs \code{run()}} +% ════════════════════════════════════════════════════════════════════════════ + +The program under analysis: + +\begin{lstlisting}[caption={StartVsRun.java}] +public class StartVsRun { + static class MyRunnable implements Runnable { + public void run() { + System.out.println("Running in: " + Thread.currentThread().getName()); + } + } + public static void main(String[] args) throws InterruptedException { + Thread t1 = new Thread(new MyRunnable(), "Thread-1"); + System.out.println("Calling run()"); + t1.run(); + Thread.sleep(100); + + Thread t2 = new Thread(new MyRunnable(), "Thread-2"); + System.out.println("Calling start()"); + t2.start(); + } +} +\end{lstlisting} + +\subsection{Expected output} + +\begin{lstlisting}[language={},numbers=none,backgroundcolor=\color{codebg}] +Calling run() +Running in: main +Calling start() +Running in: Thread-2 +\end{lstlisting} + +\subsection{Why this output?} + +\textbf{The \code{t1.run()} call:} +Calling \code{run()} on a \code{Thread} object is just an ordinary method call, exactly +like calling any other method on any other object. +No new OS thread is created or scheduled. +The body of \code{run()} executes \emph{synchronously} inside the calling thread, +which is \code{main}. +Therefore \code{Thread.currentThread().getName()} returns \texttt{"main"}, +not \texttt{"Thread-1"}. +The thread object \texttt{t1} was constructed but its native thread was never started. + +\textbf{The \code{t2.start()} call:} +\code{start()} allocates a new OS-level thread, registers it with the JVM scheduler, +and returns to the caller \emph{immediately} (it does not wait for \code{run()} to finish). +The new thread then invokes \code{run()} autonomously and concurrently. +Inside that new thread, \code{Thread.currentThread().getName()} correctly +returns \texttt{"Thread-2"}. + +\subsection{Key differences at a glance} + +\begin{center} +\begin{tabular}{>{\bfseries}lll} +\toprule +Aspect & \code{run()} & \code{start()} \\ +\midrule +New thread created? & No & Yes (exactly one) \\ +Execution thread & Current thread & New thread \\ +Blocking behaviour & Synchronous (caller waits) & Asynchronous (returns at once) \\ +Can be called twice? & Yes, legal & No – throws \code{IllegalThreadStateException} \\ +Useful for? & Testing \code{run()} logic directly & True concurrent execution \\ +\bottomrule +\end{tabular} +\end{center} + +\textbf{Summary:} +\code{run()} is simply a method call; \code{start()} is what actually creates and +launches a new thread of execution. Calling \code{run()} instead of \code{start()} is +one of the most common multithreading bugs in Java – the code compiles and +"works", but no concurrency ever happens. + +% ════════════════════════════════════════════════════════════════════════════ +\section{Daemon Threads} +% ════════════════════════════════════════════════════════════════════════════ + +\begin{lstlisting}[caption={DaemonExample.java}] +public class DaemonExample { + static class DaemonRunnable implements Runnable { + public void run() { + for (int i = 0; i < 20; i++) { + System.out.println("Daemon thread running..."); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + // [Handling Exception...] + } + } + } + } + public static void main(String[] args) { + Thread thread = new Thread(new DaemonRunnable()); + thread.setDaemon(true); + thread.start(); + System.out.println("Main thread ends."); + } +} +\end{lstlisting} + +\subsection{Expected output (with \code{setDaemon(true)})} + +\begin{lstlisting}[language={},numbers=none,backgroundcolor=\color{codebg}] +Main thread ends. +Daemon thread running... +(program terminates -- usually 0-2 more lines, non-deterministic) +\end{lstlisting} + +\textbf{Why?} +The JVM shuts down when there are no \emph{non-daemon} threads still alive. +After \code{main()} prints \texttt{"Main thread ends."} and returns, the only surviving +thread is the daemon thread. +Because it is marked as a daemon, the JVM does \emph{not} wait for it to +finish – it exits immediately (or after one scheduling quantum), abruptly +terminating the daemon thread. +The daemon may manage to print zero, one, or a few lines before termination, +depending entirely on OS thread scheduling; this is non-deterministic. + +\subsection{What happens if you remove \code{thread.setDaemon(true)}?} + +Without that call the thread becomes a regular \emph{user thread} (the default). +The JVM rule is: \textbf{wait for all user threads to finish before exiting.} +The daemon thread (now a user thread) will sleep 500 ms per iteration and +loop 20 times – the program will take approximately $20 \times 0.5 = 10$ seconds +to terminate, and all 20 lines of \texttt{"Daemon thread running..."} will be +printed in full before the JVM exits. + +\subsection{Real-life use cases of daemon threads} + +Daemon threads are ideal for background housekeeping work that should not +prevent the application from exiting cleanly. + +\begin{enumerate}[leftmargin=2em] + \item \textbf{Garbage Collection.} + The JVM's garbage collector runs as a daemon thread; + it must not keep the JVM alive on its own. + + \item \textbf{Log flushing.} + A background thread that periodically flushes buffered log entries + to disk. If the application finishes, unsaved logs are + an acceptable loss compared with hanging the process. + + \item \textbf{Cache invalidation / expiry.} + A thread that sweeps an in-memory cache and removes stale entries + does not need to outlive the application. + + \item \textbf{Heartbeat / health-check threads.} + Service-mesh sidecars and microservices often send periodic + pings to a service-discovery system. + These should die with the service, not keep it alive. + + \item \textbf{IDE background indexing.} + IntelliJ IDEA's indexer and VS Code's language-server workers + are typically daemon-like: if you close the IDE, you do not + want to wait for them to finish indexing. +\end{enumerate} + +% ════════════════════════════════════════════════════════════════════════════ +\section{A Shorter Way to Create Threads – Lambda Expressions} +% ════════════════════════════════════════════════════════════════════════════ + +\begin{lstlisting}[caption={ThreadDemo.java}] +public class ThreadDemo { + public static void main(String[] args) { + Thread thread = new Thread(() -> { + System.out.println("Thread is running using a lambda!"); + }); + + thread.start(); + } +} +\end{lstlisting} + +\subsection{Expected output} + +\begin{lstlisting}[language={},numbers=none,backgroundcolor=\color{codebg}] +Thread is running using a lambda! +\end{lstlisting} + +\subsection{What is \code{() -> \{ \ldots \}} called?} + +The \code{() -> \{ \ldots \}} syntax is called a \textbf{lambda expression} +(also known as an \emph{anonymous function} or \emph{arrow function} in +other languages). + +A lambda expression is a concise way to provide an implementation of a +\emph{functional interface} – any interface that declares exactly one abstract +method. +\code{Runnable} is a functional interface: its single abstract method is +\code{void run()}. +The lambda \code{() -> \{ \ldots \}} supplies that implementation inline, +without naming it. + +\subsection{How does this differ from other approaches?} + +There are three common ways to supply a \code{Runnable} to a \code{Thread}: + +\begin{lstlisting}[caption={Three equivalent approaches}] +// 1. Class that extends Thread +class MyThread extends Thread { + public void run() { System.out.println("extends Thread"); } +} +new MyThread().start(); + +// 2. Named class that implements Runnable +class MyRunnable implements Runnable { + public void run() { System.out.println("implements Runnable"); } +} +new Thread(new MyRunnable()).start(); + +// 3. Lambda expression (shortest) +new Thread(() -> System.out.println("lambda")).start(); +\end{lstlisting} + +\begin{center} +\begin{tabular}{>{\bfseries}lllll} +\toprule +Approach & Boilerplate & Separate class? & Captures outer vars? & Extensible? \\ +\midrule +\code{extends Thread} & High & Yes (named) & No & Inherits Thread \\ +\code{implements Runnable} & Medium & Yes (named) & No & Yes \\ +Anonymous class & Medium & Yes (anon.) & Yes (eff.\ final) & Yes \\ +Lambda & \textbf{Minimal} & No & Yes (eff.\ final) & No \\ +\bottomrule +\end{tabular} +\end{center} + +\textbf{Key advantages of lambdas:} +\begin{itemize}[leftmargin=2em] + \item \textbf{Conciseness.} + No need to declare a class, override a method, or write a constructor. + A single line can create and start a thread. + + \item \textbf{Readability.} + The intent (``run this block of code in a new thread'') is immediately + obvious to the reader. + + \item \textbf{Variable capture.} + A lambda can reference local variables from the enclosing scope as long + as they are effectively final (i.e., never reassigned after declaration). + This is often more convenient than passing data through a constructor. + + \item \textbf{Functional programming patterns.} + Lambdas work seamlessly with the Java Streams API, \code{CompletableFuture}, + \code{ExecutorService}, and other modern concurrency utilities that accept + \code{Runnable} or \code{Callable} arguments. +\end{itemize} + +\textbf{Limitation:} +Because a lambda is not a subclass of \code{Thread}, it cannot override thread +life-cycle methods such as \code{interrupt()} or \code{setUncaughtExceptionHandler()} +directly inside the lambda body. +When that level of control is needed, a named class is more appropriate. + +% ── Footer note ────────────────────────────────────────────────────────────── +\vfill +\begin{center} + \small\color{gray} + Advanced Programming – Assignment 8 \quad|\quad + Faraz Ardeh \quad|\quad + Shahid Beheshti University \quad|\quad + June 2026 +\end{center} + +\end{document} diff --git a/src/main/java/DownloadWorker.java b/src/main/java/DownloadWorker.java index 546de2d..201330b 100644 --- a/src/main/java/DownloadWorker.java +++ b/src/main/java/DownloadWorker.java @@ -2,10 +2,7 @@ import java.util.Random; /** * Simulates downloading a single chunk of a file. - * - *

This class is intentionally provided as a skeleton for students. - * The main multithreading and simulation logic should be completed - * in the run() method.

+ * Each worker writes only to its own assigned ChunkStatus object. */ public class DownloadWorker implements Runnable { @@ -21,23 +18,33 @@ public class DownloadWorker implements Runnable { @Override public void run() { - // TODO: Record the chunk start time in chunkStatus. - double downloaded = 0.0; + // Record the start time for this chunk + chunkStatus.setStartTimeMs(System.currentTimeMillis()); - // TODO: Print a message that this chunk has started downloading. + double downloaded = 0.0; + int delayRange = config.getMaxStepDelayMs() - config.getMinStepDelayMs(); + double stepRange = config.getMaxStepDownloadMB() - config.getMinStepDownloadMB(); while (downloaded < chunkStatus.getChunkSizeMB()) { - // TODO: Generate a random sleep delay between min and max delay. - // TODO: Sleep for that delay. - // TODO: Generate a random download amount for this step. - // TODO: Increase downloaded, but do not go beyond chunk size. - // TODO: Save the updated downloaded value into chunkStatus. - // TODO: Optionally print step-by-step progress. + // Random delay simulating network latency for this step + int delay = config.getMinStepDelayMs() + (delayRange > 0 ? random.nextInt(delayRange) : 0); + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + // Random amount downloaded in this step, capped at remaining size + double step = config.getMinStepDownloadMB() + random.nextDouble() * stepRange; + downloaded = Math.min(downloaded + step, chunkStatus.getChunkSizeMB()); + + // Write only to this worker's own ChunkStatus (thread-safe by design) + chunkStatus.setDownloadedMB(downloaded); } - // TODO: Mark the chunk as completed. - // TODO: Record the chunk end time in chunkStatus. - // TODO: Print a message that this chunk has finished downloading. + // Mark completion and record end time + chunkStatus.setEndTimeMs(System.currentTimeMillis()); + chunkStatus.setCompleted(true); } - } diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 82bd239..decd7b3 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -3,7 +3,7 @@ import java.util.List; public class Main { public static void main(String[] args) { - System.out.println("=== Simulated Download Manager ==="); + System.out.println("=== Simulated Download Manager ===\n"); // 1. Read config DownloadConfig config; @@ -14,94 +14,98 @@ public class Main { return; } - System.out.println("File name: " + config.getFileName()); - System.out.println("Total size (MB): " + config.getTotalSizeMB()); - System.out.println("Chunk count: " + config.getChunkCount()); + System.out.println("File name : " + config.getFileName()); + System.out.println("Total size : " + config.getTotalSizeMB() + " MB"); + System.out.println("Chunks : " + config.getChunkCount()); + System.out.println("Step delay : " + config.getMinStepDelayMs() + + "–" + config.getMaxStepDelayMs() + " ms"); + System.out.println("Step size : " + config.getMinStepDownloadMB() + + "–" + config.getMaxStepDownloadMB() + " MB"); System.out.println(); - // 2. Create chunks + // ── MULTITHREADED RUN ────────────────────────────────────────────── + + System.out.println("━━━ Multithreaded Download ━━━"); + + // 2. Create chunk status objects List chunks = ChunkUtils.createChunks( - config.getTotalSizeMB(), - config.getChunkCount() - ); + config.getTotalSizeMB(), config.getChunkCount()); - // 3. Create worker threads + // 3. Create one worker thread per chunk List workerThreads = new ArrayList<>(); - for (ChunkStatus chunk : chunks) { DownloadWorker worker = new DownloadWorker(chunk, config); - Thread workerThread = new Thread(worker, "Worker-" + chunk.getChunkId()); - - workerThreads.add(workerThread); - - // TODO: - // Students may print helpful debug information here, - // for example which chunk is assigned to which worker thread. + Thread t = new Thread(worker, "Worker-" + chunk.getChunkId()); + workerThreads.add(t); } - // 4. Create and start monitor thread + // 4. Create and start the monitor thread before workers so it is + // already polling when the first worker begins ProgressMonitor monitor = new ProgressMonitor(config, chunks); Thread monitorThread = new Thread(monitor, "Progress-Monitor"); + monitorThread.start(); - // TODO: - // Start the monitor thread before starting the workers - // so that progress can be displayed while downloading happens. - // - // Example idea: - // monitorThread.start(); + // 5. Start all worker threads + long multiStart = System.currentTimeMillis(); + for (Thread t : workerThreads) { + t.start(); + } - // 5. Start worker threads - // TODO: - // Start each worker thread in workerThreads. - // Use a loop and call start() on each thread. - - // 6. Wait for workers to finish - // TODO: - // Wait for all worker threads to complete by calling join(). - // This should be done inside a try-catch block for InterruptedException. - // - // Hint: - // for (Thread thread : workerThreads) { - // thread.join(); - // } - - // TODO: - // After all workers finish, the monitor thread may also need to stop. - // Depending on how ProgressMonitor is implemented, students may: - // - wait for it to finish on its own, or - // - add a stopping mechanism in ProgressMonitor later. - // - // If your monitor finishes automatically, you may join it here. - - // NOTE: - // this final report may show 0 progress because no worker has actually run yet. - // Until students complete the thread start/join TODOs above, + // 6. Wait for every worker to finish before printing the final report + try { + for (Thread t : workerThreads) { + t.join(); + } + // Monitor exits on its own once it detects all chunks are done; + // join here to make sure the final "All chunks completed" line is printed + monitorThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + System.out.println("Main thread interrupted while waiting for workers."); + return; + } + long multiTime = System.currentTimeMillis() - multiStart; // 7. Print final report System.out.println(); - System.out.println("=== Final Report ==="); - - int completedChunks = 0; - double downloadedMB = 0.0; - + System.out.println("━━━ Final Report ━━━"); + int completedCount = 0; + double totalDownloaded = 0.0; for (ChunkStatus chunk : chunks) { - downloadedMB += chunk.getDownloadedMB(); - - if (chunk.isCompleted()) { - completedChunks++; - } - - System.out.println( - "Chunk " + chunk.getChunkId() - + ": " + chunk.getDownloadedMB() - + "/" + chunk.getChunkSizeMB() - + " MB" - ); + totalDownloaded += chunk.getDownloadedMB(); + if (chunk.isCompleted()) completedCount++; + System.out.printf(" Chunk #%d : %.1f / %.1f MB (%.2fs)%n", + chunk.getChunkId(), + chunk.getDownloadedMB(), + chunk.getChunkSizeMB(), + chunk.getDownloadDurationMs() / 1000.0); } + System.out.printf(" Completed : %d / %d chunks%n", completedCount, chunks.size()); + System.out.printf(" Downloaded: %.1f / %.1f MB%n", + totalDownloaded, (double) config.getTotalSizeMB()); + System.out.printf(" Wall time : %.2f seconds%n%n", multiTime / 1000.0); + + // ── SEQUENTIAL RUN (Bonus – comparison) ─────────────────────────── + + System.out.println("━━━ Sequential Download (Bonus Comparison) ━━━"); + List seqChunks = ChunkUtils.createChunks( + config.getTotalSizeMB(), config.getChunkCount()); + long seqTime = SequentialDownloader.run(config, seqChunks); + + // ── COMPARISON REPORT ────────────────────────────────────────────── System.out.println(); - System.out.println("Completed chunks: " + completedChunks + "/" + chunks.size()); - System.out.println("Downloaded total: " + downloadedMB + "/" + config.getTotalSizeMB() + " MB"); + System.out.println("━━━ Performance Comparison ━━━"); + System.out.printf(" Sequential : %.2f seconds%n", seqTime / 1000.0); + System.out.printf(" Multithreaded : %.2f seconds%n", multiTime / 1000.0); + System.out.printf(" Speedup : %.2fx%n", (double) seqTime / Math.max(multiTime, 1)); + System.out.println(); + System.out.println(" Analysis:"); + System.out.println(" In the multithreaded run each worker sleeps independently,"); + System.out.println(" so all network-latency delays overlap in parallel."); + System.out.println(" The sequential run must wait for each chunk in turn,"); + System.out.println(" making the total time roughly N × (average chunk time)."); + System.out.println(); System.out.println("Simulation finished."); } } diff --git a/src/main/java/ProgressMonitor.java b/src/main/java/ProgressMonitor.java index 475c0e0..ec48477 100644 --- a/src/main/java/ProgressMonitor.java +++ b/src/main/java/ProgressMonitor.java @@ -1,5 +1,17 @@ +import java.util.ArrayDeque; +import java.util.Deque; import java.util.List; +/** + * Monitors and displays real-time download progress for all chunks. + * + * Bonus features implemented: + * - Real-time in-place progress bar using ANSI escape codes + * - Per-chunk progress bars with completion indicators + * - Overall download speed (MB/s) calculation + * - Estimated Time of Arrival (ETA) calculation + * - Event log showing chunk start/finish events + */ public class ProgressMonitor implements Runnable { private final String fileName; @@ -7,57 +19,87 @@ public class ProgressMonitor implements Runnable { private final List chunks; private final long monitorDelayMs; + // Event detection: track previous state to detect chunk start/finish + private final boolean[] wasStarted; + private final boolean[] wasCompleted; + private final Deque eventLog = new ArrayDeque<>(); + private static final int MAX_LOG_ENTRIES = 5; + + // Speed and ETA tracking + private double previousTotalMB = 0.0; + private long previousTimeMs; + + // In-place ANSI update tracking + private int lastPrintedLines = 0; + + // ANSI escape codes + private static final String RESET = "\033[0m"; + private static final String BOLD = "\033[1m"; + private static final String DIM = "\033[2m"; + private static final String GREEN = "\033[92m"; + private static final String CYAN = "\033[96m"; + private static final String YELLOW = "\033[93m"; + private static final String CLEAR_LINE = "\033[2K"; + + private static final int BAR_WIDTH = 30; + public ProgressMonitor(DownloadConfig config, List chunks) { this.fileName = config.getFileName(); this.totalSizeMB = config.getTotalSizeMB(); this.chunks = chunks; this.monitorDelayMs = 500; + this.previousTimeMs = System.currentTimeMillis(); + this.wasStarted = new boolean[chunks.size()]; + this.wasCompleted = new boolean[chunks.size()]; } @Override public void run() { - // TODO: - // Repeatedly check chunk progress until all chunks are completed. - // In each loop: - // 1. Read the downloaded size from every chunk - // 2. Add all downloaded amounts to totalDownloadedMB - // 3. Count completed chunks - // 4. Print a progress message - // 5. If completedChunks == chunks.size(), print a final monitor message and stop - // 6. Otherwise sleep for monitorDelayMs and continue - + boolean firstPrint = true; while (true) { double totalDownloadedMB = 0.0; int completedChunks = 0; - for (ChunkStatus chunk : chunks) { + // 1. Read progress from every chunk and detect state-change events + for (int i = 0; i < chunks.size(); i++) { + ChunkStatus chunk = chunks.get(i); totalDownloadedMB += chunk.getDownloadedMB(); + if (chunk.isCompleted()) completedChunks++; - if (chunk.isCompleted()) { - completedChunks++; + if (!wasStarted[i] && chunk.getStartTimeMs() > 0) { + wasStarted[i] = true; + addEvent(String.format("Chunk #%d started (%.0f MB)", + chunk.getChunkId(), chunk.getChunkSizeMB())); + } + if (!wasCompleted[i] && chunk.isCompleted()) { + wasCompleted[i] = true; + addEvent(String.format("Chunk #%d finished (%.2fs)", + chunk.getChunkId(), chunk.getDownloadDurationMs() / 1000.0)); } } - double percent = 0.0; - if (totalSizeMB > 0) { - percent = (totalDownloadedMB * 100.0) / totalSizeMB; + // 2. Calculate speed and ETA + long now = System.currentTimeMillis(); + double elapsed = (now - previousTimeMs) / 1000.0; + double speed = elapsed > 0.001 ? (totalDownloadedMB - previousTotalMB) / elapsed : 0.0; + double remaining = totalSizeMB - totalDownloadedMB; + double eta = (speed > 0.01) ? remaining / speed : -1.0; + previousTotalMB = totalDownloadedMB; + previousTimeMs = now; + + // 3. Build and print the real-time dashboard + String dashboard = buildDashboard(totalDownloadedMB, completedChunks, speed, eta); + printInPlace(dashboard, firstPrint); + firstPrint = false; + + // 5. Exit gracefully once all chunks are done + if (completedChunks == chunks.size()) { + System.out.println(BOLD + GREEN + " All chunks completed! Download finished." + RESET); + break; } - System.out.printf( - "Progress for %s: %.1f/%.1f MB (%.2f%%), completed chunks: %d/%d%n", - fileName, - totalDownloadedMB, - (double) totalSizeMB, - percent, - completedChunks, - chunks.size() - ); - - - // TODO: - // If all chunks are completed, print a final message and exit the loop - + // 6. Sleep before the next polling cycle try { Thread.sleep(monitorDelayMs); } catch (InterruptedException e) { @@ -66,4 +108,86 @@ public class ProgressMonitor implements Runnable { } } } + + private void addEvent(String message) { + if (eventLog.size() >= MAX_LOG_ENTRIES) eventLog.pollFirst(); + eventLog.addLast(message); + } + + /** + * Overwrites the previous dashboard in-place using ANSI cursor-up sequences, + * producing a smooth real-time update instead of scrolling output. + */ + private void printInPlace(String dashboard, boolean firstPrint) { + if (!firstPrint && lastPrintedLines > 0) { + // Move cursor up to the top of the previous dashboard + System.out.printf("\033[%dA", lastPrintedLines); + } + String[] lines = dashboard.split("\n", -1); + for (String line : lines) { + System.out.print(CLEAR_LINE + line + "\n"); + } + System.out.flush(); + lastPrintedLines = lines.length; + } + + private String buildDashboard(double totalMB, int completedChunks, double speed, double eta) { + StringBuilder sb = new StringBuilder(); + String divider = " " + "─".repeat(54); + + sb.append(BOLD).append(CYAN) + .append(" ┌─ Download Manager ─ ").append(fileName).append("\n") + .append(RESET); + + // Per-chunk progress rows + for (ChunkStatus chunk : chunks) { + sb.append(chunkLine(chunk)).append("\n"); + } + + sb.append(DIM).append(divider).append(RESET).append("\n"); + + // Overall progress bar + double pct = totalSizeMB > 0 ? totalMB / totalSizeMB * 100.0 : 0.0; + sb.append(String.format(" " + BOLD + "Total " + RESET + " %s %5.1f%% %.1f / %.0f MB\n", + colorBar(pct, BAR_WIDTH), pct, totalMB, (double) totalSizeMB)); + + // Speed and ETA line + if (speed > 0.01) { + String etaStr = eta >= 0 ? String.format("%.1fs", eta) : "∞"; + sb.append(String.format(" " + YELLOW + "Speed: %.2f MB/s" + RESET + + " ETA: %-8s chunks: %d / %d\n", + speed, etaStr, completedChunks, chunks.size())); + } else { + sb.append(String.format(" Warming up… chunks: %d / %d\n", + completedChunks, chunks.size())); + } + + // Event log + if (!eventLog.isEmpty()) { + sb.append(DIM).append(divider).append(RESET).append("\n"); + for (String event : eventLog) { + sb.append(DIM).append(" » ").append(event).append(RESET).append("\n"); + } + } + + // Remove trailing newline so split gives the right count + String result = sb.toString(); + if (result.endsWith("\n")) result = result.substring(0, result.length() - 1); + return result; + } + + private String chunkLine(ChunkStatus chunk) { + double pct = chunk.getProgressPercentage(); + String tick = chunk.isCompleted() ? GREEN + "✓" + RESET : " "; + return String.format(" [%s] Chunk #%d %s %5.1f%%", + tick, chunk.getChunkId(), colorBar(pct, BAR_WIDTH), pct); + } + + /** Renders a Unicode block-character progress bar with ANSI colour. */ + private String colorBar(double percent, int width) { + int filled = (int) Math.round(percent / 100.0 * width); + filled = Math.max(0, Math.min(filled, width)); + return GREEN + "█".repeat(filled) + RESET + + DIM + "░".repeat(width - filled) + RESET; + } } diff --git a/src/main/java/SequentialDownloader.java b/src/main/java/SequentialDownloader.java new file mode 100644 index 0000000..3555571 --- /dev/null +++ b/src/main/java/SequentialDownloader.java @@ -0,0 +1,32 @@ +import java.util.List; + +/** + * Bonus Task – Sequential vs Multithreaded Comparison. + * + * Downloads all chunks one after another on the calling thread. + * This exists only for timing comparison with the multithreaded run; + * it demonstrates the speedup gained from parallel execution. + */ +public class SequentialDownloader { + + private SequentialDownloader() {} + + /** + * Runs each chunk sequentially on the current thread and returns + * the total elapsed time in milliseconds. + */ + public static long run(DownloadConfig config, List chunks) { + long start = System.currentTimeMillis(); + + for (ChunkStatus chunk : chunks) { + System.out.printf("[Sequential] Chunk #%d starting (%.0f MB)%n", + chunk.getChunkId(), chunk.getChunkSizeMB()); + DownloadWorker worker = new DownloadWorker(chunk, config); + worker.run(); // blocks until this chunk is fully "downloaded" + System.out.printf("[Sequential] Chunk #%d done in %.2fs%n", + chunk.getChunkId(), chunk.getDownloadDurationMs() / 1000.0); + } + + return System.currentTimeMillis() - start; + } +}