From bcb381f84b2349867f0529f16c455bbd0be7f740 Mon Sep 17 00:00:00 2001 From: Zahra Gharagozloo Mazlaghan Date: Thu, 11 Jun 2026 02:18:41 +0330 Subject: [PATCH 1/6] Report.md created and completed. --- src/Report.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/Report.md diff --git a/src/Report.md b/src/Report.md new file mode 100644 index 0000000..d93a1c8 --- /dev/null +++ b/src/Report.md @@ -0,0 +1,95 @@ +### 1. `start()` vs `run()` + +* #### output: +```bash + Calling run() + Running in: main + Calling start() + Running in: Thread-2 +``` + +When we call `t1.run()`, Java does not create a new thread. This method runs like a normal method in the thread it was called on. That's why `Thread.currentThread().getName()` prints the name of the current thread, main. + +But when we call `t2.start()`, the JVM creates a new thread in the background. JVM creates a new thread called Thread-2 and executes the `run()` method inside this new thread. That's why the resulting thread is named Thread-2. + +* #### the difference in behavior between calling `start()` and `run()` + +The main difference between these two methods: +1. how they manage memory +2. create new trades + +`run()` method: +* No new thread +* Execution: Completely normal and line-by-line, The main thread of the program enters the run method, executes the code, and exits. Until this method finishes, subsequent lines of code in the main method wait. + +`start()` +* New thread +* Execution: Completely parallel and asynchronous. As soon as you call `start()`, the new thread starts in the background, and the main thread immediately moves on to the next line of its code without waiting for the new thread to finish. + +--- + +### 2. Daemon Threads + +* #### output: +```bash +Main thread ends. +Daemon thread running... (multiple times) +``` + +Why? + +In Java we have two types of threads: User Threads (user or main threads) and Daemon Threads (backup threads). + +The Java Virtual Machine (JVM) continues to run as long as at least one user thread is alive. Once all user threads have finished, the JVM stops the application, even if daemon threads are still running. + +In this code, the main thread is a user thread. When `thread.start()` is executed, the daemon thread starts running, but immediately on the next line, the main thread prints "Main thread ends." and terminates. Since no other user threads are alive, the JVM does not allow the daemon thread to complete its 20-thread loop; it immediately closes the entire program and the job stops. + +* #### If we remove the line `thread.setDaemon(true)` + +If we delete this line, this thread will become a user Thread and the program will no longer close immediately, and the output will be like this: + +```bash +Main thread ends. +Daemon thread running... +Daemon thread running... +... (This phrase is printed exactly 20 times) +Daemon thread running... +``` + +* #### Real-life use cases + +Daemon threads are typically used for tasks that are useful as long as the main application is open, but are no longer needed once the main application is closed. + +Some real-world examples: + +1. Garbage Collector: This trick runs continuously in the background to clear unused memory. +2. Auto-Save: A daemon thread in the background that saves our writing every few minutes. +3. Spell Checker: As we type, tread draws a red line in the background under misspelled words. + +--- + +### 3. A shorter way to create threads + +* #### output: +```bash +Thread is running using a ...! +``` + +* #### The name of this `syntax () -> { ... }` + +This structure is called a Lambda Expression in Java. + +The open and close parentheses `()` represent the method inputs (which are empty since the run method takes no parameters in its input). The arrow symbol `->` means take these inputs and put them inside the code inside the curly braces `{ ... }` to be executed. + +* #### The difference + +In terms of final performance and CPU behavior, there is no difference; the operating system creates a new thread in both cases. But the differences: + +1. Eliminate Boilerplate Code: In the old way, we had to create a new class, write `public void run()` inside it, and add a bunch of opening and closing braces. Lambda removes all of this formality and lets us get straight to the “boilerplate” (the code that needs to be executed). +2. Using Functional Interface: Java is very smart. Since the Thread class constructor takes an input of type Runnable interface and this interface has only one method called `run()`, Java automatically understands that the code inside the lambda is going to replace that one `run()` method. +3. Preserve inheritance: When we write a class that extends Thread, due to Java limitations, we cannot inherit that class from any other class. But the lambda method (which is based on Runnable) does not impose this limitation and leaves us free to design our classes. +4. Separating the task from the executor: In this method, we inject the task into the execution engine (Thread) as a body of code, which is much more standard than merging the two together. + +--- + +Done :) \ No newline at end of file -- 2.54.0 From b2bf918030f15daf1d14bc6a949d940ea43366a4 Mon Sep 17 00:00:00 2001 From: Zahra Gharagozloo Mazlaghan Date: Thu, 11 Jun 2026 23:23:01 +0330 Subject: [PATCH 2/6] DownloadWorker completed. --- src/main/java/ChunkStatus.java | 4 +++ src/main/java/DownloadConfig.java | 6 ++++ src/main/java/DownloadWorker.java | 56 +++++++++++++++++++++++++------ 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/main/java/ChunkStatus.java b/src/main/java/ChunkStatus.java index 3e902dc..b17eb70 100644 --- a/src/main/java/ChunkStatus.java +++ b/src/main/java/ChunkStatus.java @@ -105,4 +105,8 @@ public class ChunkStatus { completed ? " [Completed]" : "" ); } + + public void setStartTime(long startTime) { + + } } diff --git a/src/main/java/DownloadConfig.java b/src/main/java/DownloadConfig.java index dc1ba3a..f6a1745 100644 --- a/src/main/java/DownloadConfig.java +++ b/src/main/java/DownloadConfig.java @@ -67,4 +67,10 @@ public class DownloadConfig { ", maxStepDownloadMB=" + maxStepDownloadMB + '}'; } + + public int getMinDelayMs() { + } + + public int getMaxDelayMs() { + } } diff --git a/src/main/java/DownloadWorker.java b/src/main/java/DownloadWorker.java index 546de2d..a75be09 100644 --- a/src/main/java/DownloadWorker.java +++ b/src/main/java/DownloadWorker.java @@ -21,23 +21,57 @@ public class DownloadWorker implements Runnable { @Override public void run() { - // TODO: Record the chunk start time in chunkStatus. + long startTime = System.currentTimeMillis(); + chunkStatus.setStartTime(startTime); + double downloaded = 0.0; - // TODO: Print a message that this chunk has started downloading. + System.out.println("Chunk \"" + chunkStatus.getChunkId() + "\" started downloading."); 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. + + try { + //Generate a random sleep delay between min and max delay. + int minDelay = config.getMinDelayMs(); + int maxDelay = config.getMaxDelayMs(); + int randomDelay = random.nextInt((maxDelay - minDelay) + 1) + minDelay; + + //Sleep for that delay. + Thread.sleep(randomDelay); + + //Generate a random download amount for this step. + double minStep = config.getMinStepDelayMs(); + double maxStep = config.getMaxStepDelayMs(); + double randomStep = (maxStep + minStep)*random.nextDouble() + minStep; + + //Increase downloaded, but do not go beyond chunk size. + downloaded += randomStep; + if (downloaded > chunkStatus.getChunkSizeMB()) { + downloaded = chunkStatus.getChunkSizeMB(); + } + + //Save the updated downloaded value into chunkStatus. + chunkStatus.setDownloadedMB(downloaded); + + //print step-by-step progress. + System.out.printf("Chunk #%d: Downloaded %.2f / %.2f MB%n", + chunkStatus.getChunkId(), downloaded, chunkStatus.getChunkSizeMB()); + + } catch (InterruptedException e) { + System.out.println("Chunk \"" + chunkStatus.getChunkId() + "\" was interrupted!"); + return; + } } - // 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 the chunk as completed. + chunkStatus.setCompleted(true); + + //Record the chunk end time in chunkStatus. + long endTime = System.currentTimeMillis(); + chunkStatus.setEndTimeMs(endTime); + + //Print a message that this chunk has finished downloading. + System.out.println("Chunk \"" + chunkStatus.getChunkId() + "\" has finished downloading."); } } -- 2.54.0 From fd4b23240832d98b2e4f72d6621f214ef11ac268 Mon Sep 17 00:00:00 2001 From: Zahra Gharagozloo Mazlaghan Date: Fri, 12 Jun 2026 12:02:47 +0330 Subject: [PATCH 3/6] updated ChunkStatus, ConfigReader, DownloadConfig. --- src/main/java/ChunkStatus.java | 2 +- src/main/java/ConfigReader.java | 12 +++++++++++- src/main/java/DownloadConfig.java | 8 +++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/ChunkStatus.java b/src/main/java/ChunkStatus.java index b17eb70..b30e3e8 100644 --- a/src/main/java/ChunkStatus.java +++ b/src/main/java/ChunkStatus.java @@ -107,6 +107,6 @@ public class ChunkStatus { } public void setStartTime(long startTime) { - + this.startTimeMs = startTime; } } diff --git a/src/main/java/ConfigReader.java b/src/main/java/ConfigReader.java index 5754060..1eae77d 100644 --- a/src/main/java/ConfigReader.java +++ b/src/main/java/ConfigReader.java @@ -20,6 +20,8 @@ public class ConfigReader { int maxStepDelayMs = 0; double minStepDownloadMB = 0; double maxStepDownloadMB = 0; + int minDelayMs = 0; + int maxDelayMs = 0; try (BufferedReader reader = new BufferedReader( new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { @@ -62,6 +64,12 @@ public class ConfigReader { case "maxStepDownloadMB": maxStepDownloadMB = Double.parseDouble(value); break; + case "minDelayMs": + minDelayMs = Integer.parseInt(value); + break; + case "maxDelayMs": + maxDelayMs = Integer.parseInt(value); + break; default: // Ignore unknown keys to keep parsing simple break; @@ -79,7 +87,9 @@ public class ConfigReader { minStepDelayMs, maxStepDelayMs, minStepDownloadMB, - maxStepDownloadMB + maxStepDownloadMB, + minDelayMs, + maxDelayMs ); } } diff --git a/src/main/java/DownloadConfig.java b/src/main/java/DownloadConfig.java index f6a1745..e765d75 100644 --- a/src/main/java/DownloadConfig.java +++ b/src/main/java/DownloadConfig.java @@ -10,13 +10,15 @@ public class DownloadConfig { private final int maxStepDelayMs; private final double minStepDownloadMB; private final double maxStepDownloadMB; + private final int maxDelayMs; + private final int minDelayMs; /** * Constructs a new DownloadConfig with specified simulation parameters. */ public DownloadConfig(String fileName, int totalSizeMB, int chunkCount, int minStepDelayMs, int maxStepDelayMs, - double minStepDownloadMB, double maxStepDownloadMB) { + double minStepDownloadMB, double maxStepDownloadMB, int maxDelayMs, int minDelayMs) { this.fileName = fileName; this.totalSizeMB = totalSizeMB; this.chunkCount = chunkCount; @@ -24,6 +26,8 @@ public class DownloadConfig { this.maxStepDelayMs = maxStepDelayMs; this.minStepDownloadMB = minStepDownloadMB; this.maxStepDownloadMB = maxStepDownloadMB; + this.maxDelayMs = maxDelayMs; + this.minDelayMs = minDelayMs; } // Getters @@ -69,8 +73,10 @@ public class DownloadConfig { } public int getMinDelayMs() { + return minDelayMs; } public int getMaxDelayMs() { + return maxDelayMs; } } -- 2.54.0 From 11c10175daa5a4490b4aa241b6fcab6ff6ff82c3 Mon Sep 17 00:00:00 2001 From: Zahra Gharagozloo Mazlaghan Date: Fri, 12 Jun 2026 12:13:05 +0330 Subject: [PATCH 4/6] ProgressMonitor completed. --- src/main/java/ProgressMonitor.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/main/java/ProgressMonitor.java b/src/main/java/ProgressMonitor.java index 475c0e0..012b9bc 100644 --- a/src/main/java/ProgressMonitor.java +++ b/src/main/java/ProgressMonitor.java @@ -16,16 +16,6 @@ public class ProgressMonitor implements Runnable { @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 - while (true) { double totalDownloadedMB = 0.0; @@ -54,9 +44,10 @@ public class ProgressMonitor implements Runnable { chunks.size() ); - - // TODO: - // If all chunks are completed, print a final message and exit the loop + if (completedChunks == chunks.size()) { + System.out.println("All chunks have completed downloading. Monitoring stopped."); + break; + } try { Thread.sleep(monitorDelayMs); -- 2.54.0 From dd013d1c44351da507fa9e116e331a2567631a4d Mon Sep 17 00:00:00 2001 From: Zahra Gharagozloo Mazlaghan Date: Fri, 12 Jun 2026 12:41:55 +0330 Subject: [PATCH 5/6] Main class completed. --- src/main/java/Main.java | 49 +++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 82bd239..eb711d4 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -34,48 +34,35 @@ public class Main { workerThreads.add(workerThread); - // TODO: - // Students may print helpful debug information here, - // for example which chunk is assigned to which worker thread. + System.out.printf("[Debug] Assigned Chunk #%d (%.1f MB) to Thread: %s%n", + chunk.getChunkId(), chunk.getChunkSizeMB(), workerThread.getName()); } // 4. Create and start monitor thread ProgressMonitor monitor = new ProgressMonitor(config, chunks); Thread monitorThread = new Thread(monitor, "Progress-Monitor"); - // TODO: - // Start the monitor thread before starting the workers - // so that progress can be displayed while downloading happens. - // - // Example idea: - // monitorThread.start(); + System.out.println("\n[Main] Starting Progress Monitor..."); + monitorThread.start(); // 5. Start worker threads - // TODO: - // Start each worker thread in workerThreads. - // Use a loop and call start() on each thread. + System.out.println("[Main] Activating worker threads for parallel downloading...\n"); + for (Thread thread : workerThreads) { + thread.start(); + } // 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(); - // } + try { + System.out.println("[Main] Waiting for all download workers to complete..."); + 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, + System.out.println("[Main] Workers finished. Waiting for final monitor log..."); + monitorThread.join(); + } catch (InterruptedException e) { + System.out.println("Main thread was interrupted while waiting for workers: " + e.getMessage()); + } // 7. Print final report System.out.println(); -- 2.54.0 From 219f008088b8a69f4c5957c235bc9d13302ed004 Mon Sep 17 00:00:00 2001 From: Zahra Gharagozloo Mazlaghan Date: Fri, 12 Jun 2026 17:28:23 +0330 Subject: [PATCH 6/6] Bonus tasks completed. --- src/main/java/Main.java | 121 ++++++++++++++++++++++------- src/main/java/ProgressMonitor.java | 40 +++++++++- 2 files changed, 132 insertions(+), 29 deletions(-) diff --git a/src/main/java/Main.java b/src/main/java/Main.java index eb711d4..a3f67ac 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -1,5 +1,6 @@ import java.util.ArrayList; import java.util.List; +import java.util.Scanner; public class Main { public static void main(String[] args) { @@ -19,55 +20,122 @@ public class Main { System.out.println("Chunk count: " + config.getChunkCount()); System.out.println(); + Scanner scanner = new Scanner(System.in); + System.out.println("Select Download Mode:"); + System.out.println("1. Multithreaded Mode (Parallel Downloading)"); + System.out.println("2. Sequential Mode (One by One)"); + System.out.println("3. Run Performance Benchmark (Compare Both)"); + System.out.print("Your Choice (1-3): "); + int choice = scanner.nextInt(); + + switch (choice) { + case 1 -> { + System.out.println("\n--- Starting Multithreaded Download ---"); + runMultithreaded(config); + } + case 2 -> { + System.out.println("\n--- Starting Sequential Download ---"); + runSequential(config); + } + case 3 -> { + System.out.println("\n--- Running Performance Benchmark ---"); + + System.out.println("\n[Test 1/2] Executing Sequential Mode..."); + long sequentialTime = runSequential(config); + + try { Thread.sleep(1000); } catch (InterruptedException ignored) {} + + System.out.println("\n[Test 2/2] Executing Multithreaded Mode..."); + long multithreadedTime = runMultithreaded(config); + + System.out.println("\n============================================="); + System.out.println("BENCHMARK COMPARISON REPORT"); + System.out.println("============================================="); + System.out.printf("Sequential Execution Time : %d ms%n", sequentialTime); + System.out.printf("Multithreaded Execution Time: %d ms%n", multithreadedTime); + + double speedup = (double) sequentialTime / multithreadedTime; + System.out.printf("Multithreaded Mode is %.2fx faster!%n", speedup); + System.out.println("============================================="); + } + default -> System.out.println("Invalid choice. Exiting simulation."); + } + } + + //download as multi-thread + private static long runMultithreaded(DownloadConfig config) { + long startTime = System.currentTimeMillis(); + // 2. Create chunks - List chunks = ChunkUtils.createChunks( - config.getTotalSizeMB(), - config.getChunkCount() - ); + List chunks = ChunkUtils.createChunks(config.getTotalSizeMB(), config.getChunkCount()); // 3. Create worker threads 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); - - System.out.printf("[Debug] Assigned Chunk #%d (%.1f MB) to Thread: %s%n", - chunk.getChunkId(), chunk.getChunkSizeMB(), workerThread.getName()); } // 4. Create and start monitor thread ProgressMonitor monitor = new ProgressMonitor(config, chunks); Thread monitorThread = new Thread(monitor, "Progress-Monitor"); - - System.out.println("\n[Main] Starting Progress Monitor..."); monitorThread.start(); // 5. Start worker threads - System.out.println("[Main] Activating worker threads for parallel downloading...\n"); for (Thread thread : workerThreads) { thread.start(); } - // 6. Wait for workers to finish + // 6. Wait for workers and monitor to finish try { - System.out.println("[Main] Waiting for all download workers to complete..."); for (Thread thread : workerThreads) { thread.join(); } - - System.out.println("[Main] Workers finished. Waiting for final monitor log..."); monitorThread.join(); + System.out.println(); } catch (InterruptedException e) { - System.out.println("Main thread was interrupted while waiting for workers: " + e.getMessage()); + System.out.println("Multithreaded mode interrupted."); } - // 7. Print final report - System.out.println(); - System.out.println("=== Final Report ==="); + long endTime = System.currentTimeMillis(); + printFinalReport(chunks, config.getTotalSizeMB()); + return (endTime - startTime); + } + //Running download simulation sequentially + private static long runSequential(DownloadConfig config) { + long startTime = System.currentTimeMillis(); + + // 2. Create chunks + List chunks = ChunkUtils.createChunks(config.getTotalSizeMB(), config.getChunkCount()); + + // 4. Create and start monitor thread + ProgressMonitor monitor = new ProgressMonitor(config, chunks); + Thread monitorThread = new Thread(monitor, "Progress-Monitor"); + monitorThread.start(); + + // 5. Execute workers sequentially on the main thread + for (ChunkStatus chunk : chunks) { + DownloadWorker worker = new DownloadWorker(chunk, config); + worker.run(); + } + + // 6. Wait for monitor to finish logging + try { + monitorThread.join(); + System.out.println(); + } catch (InterruptedException e) { + System.out.println("Sequential mode interrupted."); + } + + long endTime = System.currentTimeMillis(); + printFinalReport(chunks, config.getTotalSizeMB()); + return (endTime - startTime); + } + + private static void printFinalReport(List chunks, int totalSizeMB) { + System.out.println("=== Final Report ==="); int completedChunks = 0; double downloadedMB = 0.0; @@ -78,17 +146,14 @@ public class Main { completedChunks++; } - System.out.println( - "Chunk " + chunk.getChunkId() - + ": " + chunk.getDownloadedMB() - + "/" + chunk.getChunkSizeMB() - + " MB" - ); + System.out.printf("Chunk %d: %.2f / %.2f MB (Completed: %b)%n", + chunk.getChunkId(), chunk.getDownloadedMB(), chunk.getChunkSizeMB(), chunk.isCompleted()); } System.out.println(); System.out.println("Completed chunks: " + completedChunks + "/" + chunks.size()); - System.out.println("Downloaded total: " + downloadedMB + "/" + config.getTotalSizeMB() + " MB"); + System.out.printf("Downloaded total: %.2f / %.2f MB%n", downloadedMB, (double) totalSizeMB); System.out.println("Simulation finished."); + System.out.println("-------------------------------------------------"); } -} +} \ No newline at end of file diff --git a/src/main/java/ProgressMonitor.java b/src/main/java/ProgressMonitor.java index 012b9bc..ef1bfb6 100644 --- a/src/main/java/ProgressMonitor.java +++ b/src/main/java/ProgressMonitor.java @@ -1,7 +1,8 @@ import java.util.List; public class ProgressMonitor implements Runnable { - + private final DownloadConfig config; + private final long startTime; private final String fileName; private final int totalSizeMB; private final List chunks; @@ -12,10 +13,15 @@ public class ProgressMonitor implements Runnable { this.totalSizeMB = config.getTotalSizeMB(); this.chunks = chunks; this.monitorDelayMs = 500; + this.startTime = System.currentTimeMillis(); + this.config = config; } @Override public void run() { + double lastDownloadedMB = 0.0; + long lastCheckTime = System.currentTimeMillis(); + int monitorDelayMs = config.getMinStepDelayMs(); while (true) { double totalDownloadedMB = 0.0; @@ -29,10 +35,42 @@ public class ProgressMonitor implements Runnable { } } + long currentTime = System.currentTimeMillis(); + double timePassedSeconds = (currentTime - lastCheckTime) / 1000.0; + + double currentSpeed = 0.0; + if (timePassedSeconds > 0) { + currentSpeed = (totalDownloadedMB - lastDownloadedMB) / timePassedSeconds; + if (currentSpeed < 0) currentSpeed = 0; + } + + double remainingMB = config.getTotalSizeMB() - totalDownloadedMB; + String etaStr = "Calculating..."; + if (currentSpeed > 0) { + int etaSeconds = (int) (remainingMB / currentSpeed); + etaStr = String.format("%ds", etaSeconds); + } else if (remainingMB <= 0) { + etaStr = "0s"; + } + + lastDownloadedMB = totalDownloadedMB; + lastCheckTime = currentTime; + + //Progress Bar double percent = 0.0; if (totalSizeMB > 0) { percent = (totalDownloadedMB * 100.0) / totalSizeMB; } + int barLength = 30; + int filledLength = (int) (barLength * (percent / 100.0)); + + StringBuilder progressBar = new StringBuilder("["); + for (int i = 0; i < barLength; i++) { + if (i < filledLength) progressBar.append("="); + else if (i == filledLength) progressBar.append(">"); + else progressBar.append(" "); + } + progressBar.append("]"); System.out.printf( "Progress for %s: %.1f/%.1f MB (%.2f%%), completed chunks: %d/%d%n", -- 2.54.0