From e446d780cc26dc33708b7b3ca58fc1c98160e51a Mon Sep 17 00:00:00 2001 From: linyimin0812 Date: Tue, 10 Oct 2023 00:34:29 +0800 Subject: [PATCH 1/2] feat: use directory-watcher to improve file watching performance --- spring-startup-cli/pom.xml | 13 +- .../recompile/ModifiedFileProcessor.java | 16 +-- .../recompile/ModifiedFileWatcher.java | 122 ++++-------------- .../spring/startup/utils/ShellUtil.java | 2 +- 4 files changed, 42 insertions(+), 111 deletions(-) diff --git a/spring-startup-cli/pom.xml b/spring-startup-cli/pom.xml index ef7fe05..1c3bd79 100644 --- a/spring-startup-cli/pom.xml +++ b/spring-startup-cli/pom.xml @@ -11,8 +11,8 @@ UTF-8 - 17 - 17 + 1.8 + 1.8 io.github.linyimin0812.spring.startup.cli.CliMain 0.9.27 spring-startup-cli @@ -44,7 +44,14 @@ 2.0.40 - + + + io.methvin + directory-watcher + 0.18.0 + + + org.junit.jupiter junit-jupiter RELEASE diff --git a/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/recompile/ModifiedFileProcessor.java b/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/recompile/ModifiedFileProcessor.java index 3487897..9d0a6f1 100644 --- a/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/recompile/ModifiedFileProcessor.java +++ b/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/recompile/ModifiedFileProcessor.java @@ -8,6 +8,7 @@ import io.github.linyimin0812.spring.startup.jdwp.command.RedefineClassesCommand; import io.github.linyimin0812.spring.startup.jdwp.command.RedefineClassesReplyPackage; import io.github.linyimin0812.spring.startup.utils.ModuleUtil; +import io.methvin.watcher.DirectoryChangeEvent; import java.io.File; import java.io.IOException; @@ -20,30 +21,29 @@ import java.util.concurrent.ConcurrentHashMap; import static io.github.linyimin0812.spring.startup.constant.Constants.OUT; -import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; /** * @author linyimin **/ public class ModifiedFileProcessor { - private final Map> FILE_WATCH_EVENTS = new ConcurrentHashMap<>(); + private final Map FILE_WATCH_EVENTS = new ConcurrentHashMap<>(); private final Map RECOMIPLED_FILE_MAP = new ConcurrentHashMap<>(); public ModifiedFileProcessor() { // TODO: 初始化时从git获取所有修改文件 } - public void onEvent(Path dir, WatchEvent.Kind eventKind) { + public void onEvent(DirectoryChangeEvent event) { - String path = dir.toString(); + String path = event.path().toString(); if (!FILE_WATCH_EVENTS.containsKey(path)) { - OUT.printf("\n[INFO] - [%s] %s\n", eventKind.name().replace("ENTRY_", Constants.EMPTY_STRING), path); + OUT.printf("\n[INFO] - [%s] %s\n", event.eventType().name(), path); OUT.printf(CliMain.prompt()); } - FILE_WATCH_EVENTS.put(path, eventKind); + FILE_WATCH_EVENTS.put(path, event); } /** @@ -76,7 +76,7 @@ public boolean check() { return false; } - boolean anyAdded = FILE_WATCH_EVENTS.values().stream().anyMatch(kind -> kind == ENTRY_CREATE); + boolean anyAdded = FILE_WATCH_EVENTS.values().stream().anyMatch(event -> event.eventType() == DirectoryChangeEvent.EventType.CREATE); if (anyAdded) { OUT.println("Hotswap does not support adding new files, please restart the application"); @@ -176,7 +176,7 @@ private RedefineClassesCommand buildRedefineClassesCommand(Map loa String fileNameWithoutPrefix = changeFile.getFileName().toString().replace(Constants.SOURCE_PREFIX, Constants.EMPTY_STRING); - Files.walkFileTree(Paths.get(compilePath), new SimpleFileVisitor<>() { + Files.walkFileTree(Paths.get(compilePath), new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { diff --git a/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/recompile/ModifiedFileWatcher.java b/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/recompile/ModifiedFileWatcher.java index ffd65bd..3425e4e 100644 --- a/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/recompile/ModifiedFileWatcher.java +++ b/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/recompile/ModifiedFileWatcher.java @@ -3,52 +3,21 @@ import io.github.linyimin0812.spring.startup.constant.Constants; import io.github.linyimin0812.spring.startup.utils.ModuleUtil; import io.github.linyimin0812.spring.startup.utils.StringUtil; +import io.methvin.watcher.DirectoryWatcher; import java.io.IOException; import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.HashMap; import java.util.List; -import java.util.Map; - -import static io.github.linyimin0812.spring.startup.constant.Constants.OUT; -import static java.nio.file.LinkOption.NOFOLLOW_LINKS; -import static java.nio.file.StandardWatchEventKinds.*; +import java.util.stream.Collectors; /** * @author linyimin **/ public class ModifiedFileWatcher { - private final WatchService watcher; - private final Map keys; - - private final ModifiedFileProcessor processor; - - private boolean running = true; - - /** - * Register the given directory, and all its subdirectories, with the - * WatchService. - */ - private void registerAll(final Path start) throws IOException { - - if (!start.toFile().exists()) { - return; - } - - // register directory and subdirectories - Files.walkFileTree(start, new SimpleFileVisitor<>() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + private final DirectoryWatcher watcher; - WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); - keys.put(key, dir); - - return FileVisitResult.CONTINUE; - } - }); - } + public boolean running = false; public ModifiedFileWatcher(ModifiedFileProcessor processor) throws IOException { this(System.getProperty(Constants.USER_DIR), processor); @@ -61,81 +30,36 @@ public ModifiedFileWatcher(String dir, ModifiedFileProcessor processor) throws I Path path = Paths.get(dir); - this.keys = new HashMap<>(); - this.processor = processor; - this.watcher = FileSystems.getDefault().newWatchService(); - List moduleHomes = ModuleUtil.getModulePaths(path); + List moduleSourceDirs = moduleHomes.stream().map(moduleHome -> moduleHome.resolve(Constants.SOURCE_DIR)).filter(Files::exists).collect(Collectors.toList()); + + this.watcher = DirectoryWatcher.builder() + .paths(moduleSourceDirs) + .listener(processor::onEvent) + .build(); + int longest = moduleHomes.stream().map(Path::toString).map(String::length).max(Integer::compareTo).orElse(0) + 32; for (Path moduleHome : moduleHomes) { - OUT.format("[INFO] %s WATCHING\n", StringUtil.rightPad(moduleHome.toString() + Constants.SPACE, longest, ".")); - registerAll(moduleHome.resolve(Constants.SOURCE_DIR)); + System.out.format("[INFO] %s WATCHING\n", StringUtil.rightPad(moduleHome.toString() + Constants.SPACE, longest, ".")); } - new Thread(this::processEvents).start(); - } - - /** - * Process all events for keys queued to the watcher - */ - private void processEvents() { - - while (running) { - - // wait for key to be signalled - WatchKey key; - try { - key = watcher.take(); - } catch (InterruptedException | ClosedWatchServiceException ignored) { - OUT.println("[INFO] File-Watcher closed"); - return; - } - - Path dir = keys.get(key); - if (dir == null) { - System.err.println("WatchKey not recognized!!"); - continue; - } - - for (WatchEvent event: key.pollEvents()) { - - WatchEvent.Kind kind = event.kind(); - - // TBD - provide example of how OVERFLOW event is handled - if (kind == OVERFLOW) { - continue; - } - - // Context for directory entry event is the file name of entry - @SuppressWarnings("unchecked") - WatchEvent ev = (WatchEvent) event; - Path child = dir.resolve(ev.context()); - - if (Files.isDirectory(child, NOFOLLOW_LINKS)) { - continue; - } + new Thread(watcher::watch).start(); - this.processor.onEvent(child, ev.kind()); - - } - - // reset key and remove from set if directory no longer accessible - boolean valid = key.reset(); - if (!valid) { - keys.remove(key); - - // all directories are inaccessible - if (keys.isEmpty()) { - break; - } - } - } + running = true; } public void close() throws IOException { - this.running = false; + running = false; this.watcher.close(); } + + public static void main(String[] args) throws IOException, InterruptedException { + ModifiedFileWatcher recompileFileWatcher = new ModifiedFileWatcher("/Users/banzhe/IdeaProjects/project/spring-boot-async-bean-demo/", new ModifiedFileProcessor()); + + Thread.sleep(20 * 1000); + + recompileFileWatcher.close(); + } } diff --git a/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/utils/ShellUtil.java b/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/utils/ShellUtil.java index 6bdd313..ddd3a32 100644 --- a/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/utils/ShellUtil.java +++ b/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/utils/ShellUtil.java @@ -35,7 +35,7 @@ public static Result execute(String[] cmdArray, boolean print) { } } - String content = !sb.isEmpty() ? sb.substring(0, sb.length() - 1) : Constants.EMPTY_STRING; + String content = sb.length() > 0 ? sb.substring(0, sb.length() - 1) : Constants.EMPTY_STRING; return new Result(code, content); } From caf19330112e8d7aabdcd2d1318f0910d65b1089 Mon Sep 17 00:00:00 2001 From: linyimin Date: Sun, 15 Oct 2023 10:22:12 +0000 Subject: [PATCH 2/2] feat: directory-watcher supports native image build --- .github/workflows/release.yml | 2 +- spring-startup-cli/pom.xml | 47 ------------------- .../spring/startup/cli/CliMain.java | 28 ++++++++++- 3 files changed, 28 insertions(+), 49 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99dc5b1..2b89cd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -147,7 +147,7 @@ jobs: run: | cd ./spring-startup-cli mvn clean package - mvn -Pnative -Dagent exec:exec@java-agent + java -agentlib:native-image-agent=config-output-dir=./target/native/agent-output/main/ -jar ./target/spring-startup-cli-1.0-SNAPSHOT-jar-with-dependencies.jar exec:exec@java-agent mvn -DskipTests=true -Pnative -Dagent package cp ./target/spring-startup-cli${{ matrix.binaryExt }} ./target/spring-startup-cli-${{ matrix.name }}${{ matrix.binaryExt }} diff --git a/spring-startup-cli/pom.xml b/spring-startup-cli/pom.xml index 1c3bd79..3761cd4 100644 --- a/spring-startup-cli/pom.xml +++ b/spring-startup-cli/pom.xml @@ -64,20 +64,6 @@ org.apache.maven.plugins maven-compiler-plugin 3.8.1 - - - - info.picocli - picocli-codegen - 4.7.5 - - - - -Aproject=${project.groupId}/${project.artifactId} - - ${maven.compiler.source} - ${maven.compiler.source} - @@ -173,39 +159,6 @@ ${imageName} - - org.codehaus.mojo - exec-maven-plugin - 3.0.0 - - - java-agent - - exec - - - java - ${project.build.directory} - - -classpath - - ${mainClass} - exec:exec@java-agent - - - - - native - - exec - - - ${project.build.directory}/${imageName} - ${project.build.directory} - - - - diff --git a/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/cli/CliMain.java b/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/cli/CliMain.java index e32885f..f00459f 100644 --- a/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/cli/CliMain.java +++ b/spring-startup-cli/src/main/java/io/github/linyimin0812/spring/startup/cli/CliMain.java @@ -19,6 +19,7 @@ import picocli.shell.jline3.PicocliCommands.PicocliCommandsFactory; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; @@ -42,7 +43,7 @@ public static void main(String[] args) { // only for run mvn -Pnative -Dagent exec:exec@java-agent if (args.length > 0 && "exec:exec@java-agent".equals(args[0])) { - commands.close(); + forNativeTracingAgent(); return; } @@ -115,4 +116,29 @@ public static String prompt() { return Constants.CLI_NAME + " (" + currentBranch + ") > "; } + + private static void forNativeTracingAgent() { + + Path file = Paths.get(System.getProperty(Constants.USER_DIR), Constants.SOURCE_DIR, "forNativeTracingAgent.java"); + + new Thread(() -> { + try { + + Files.deleteIfExists(file); + + Files.createFile(file); + + Thread.sleep(1000); + + } catch (IOException | InterruptedException ignored) { + } finally { + try { + commands.close(); + Files.deleteIfExists(file); + + } catch (IOException ignored) { + } + } + }).start(); + } } \ No newline at end of file