From bf4dbda25e87448558ea223d902a275ecd19d518 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Sun, 8 Oct 2017 15:24:23 -0400 Subject: [PATCH 1/3] feat(file_watcher): Debounce file changes The original impetus for this is an apparent bug with inotifywait on linux where every time I save a file four file change events are fired. Even if that bug is fixed, though, it's still nice to debounce file change events so that operations which change files a lot (git rebase, for example) only end up running one test run. I played with the debounce timer a lot and at least on my machine 100ms provides a pretty nice balance of responsiveness to file save without ever really seeing double-test-runs. --- lib/cortex/file_watcher.ex | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/cortex/file_watcher.ex b/lib/cortex/file_watcher.ex index 425213e..81fac0e 100644 --- a/lib/cortex/file_watcher.ex +++ b/lib/cortex/file_watcher.ex @@ -8,6 +8,7 @@ defmodule Cortex.FileWatcher do use GenServer @watched_dirs ["lib/", "test/", "apps/"] + @debounce_timeout 100 ########################################## # Public API @@ -28,14 +29,25 @@ defmodule Cortex.FileWatcher do FileSystem.subscribe(watcher_pid) - {:ok, %{watcher_pid: watcher_pid}} + {:ok, %{watcher_pid: watcher_pid, debounce_timers: %{}}} end def handle_info({:file_event, watcher_pid, {path, _events}}, - %{watcher_pid: watcher_pid} = state) do + %{watcher_pid: watcher_pid, debounce_timers: debounce_timers} = state) do + with {:ok, old_timer} <- Map.fetch(debounce_timers, path) do + Process.cancel_timer(old_timer) + end + + timer = + Process.send_after(self(), {:debounce_timer_complete, path}, @debounce_timeout) + + {:noreply, put_in(state[:debounce_timers][path], timer)} + end + + def handle_info({:debounce_timer_complete, path}, %{debounce_timers: debounce_timers} = state) do GenServer.cast(Controller, {:file_changed, file_type(path), path}) - {:noreply, state} + {:noreply, %{state | debounce_timers: Map.delete(debounce_timers, path)}} end def handle_info({:file_event, watcher_pid, :stop}, %{watcher_pid: watcher_pid} = state) do From 55beee301ea65d1a239976b63e7b4eb527c03493 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Sun, 8 Oct 2017 15:29:52 -0400 Subject: [PATCH 2/3] style(): Don't use parens for nullary funs --- lib/cortex/test_runner.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cortex/test_runner.ex b/lib/cortex/test_runner.ex index 0fbe231..a0e8d47 100644 --- a/lib/cortex/test_runner.ex +++ b/lib/cortex/test_runner.ex @@ -135,7 +135,7 @@ defmodule Cortex.TestRunner do do: ExUnit.configure(exclude: [:test], include: focus) - defp clear_focus(), + defp clear_focus, do: ExUnit.configure(exclude: [], include: []) From fd0cb8e5a49ab51aa131245ed105f0281aeef474 Mon Sep 17 00:00:00 2001 From: Griffin Smith Date: Mon, 9 Oct 2017 11:39:44 -0400 Subject: [PATCH 3/3] refactor(file-watcher): Throttle rather than debouncing To avoid a delay before running the tests, use a throttle operation rather than debouncing - this means that within a time window the *first* file change event will trigger a test run, rather than the last. --- lib/cortex/file_watcher.ex | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/cortex/file_watcher.ex b/lib/cortex/file_watcher.ex index 81fac0e..7d13e2d 100644 --- a/lib/cortex/file_watcher.ex +++ b/lib/cortex/file_watcher.ex @@ -8,7 +8,7 @@ defmodule Cortex.FileWatcher do use GenServer @watched_dirs ["lib/", "test/", "apps/"] - @debounce_timeout 100 + @throttle_timer 100 ########################################## # Public API @@ -29,25 +29,21 @@ defmodule Cortex.FileWatcher do FileSystem.subscribe(watcher_pid) - {:ok, %{watcher_pid: watcher_pid, debounce_timers: %{}}} + {:ok, %{watcher_pid: watcher_pid, throttling: false}} end def handle_info({:file_event, watcher_pid, {path, _events}}, - %{watcher_pid: watcher_pid, debounce_timers: debounce_timers} = state) do - with {:ok, old_timer} <- Map.fetch(debounce_timers, path) do - Process.cancel_timer(old_timer) + %{watcher_pid: watcher_pid, throttling: throttling} = state) do + unless throttling do + GenServer.cast(Controller, {:file_changed, file_type(path), path}) + Process.send_after(self(), :throttle_timer_complete, @throttle_timer) end - timer = - Process.send_after(self(), {:debounce_timer_complete, path}, @debounce_timeout) - - {:noreply, put_in(state[:debounce_timers][path], timer)} + {:noreply, %{state | throttling: true}} end - def handle_info({:debounce_timer_complete, path}, %{debounce_timers: debounce_timers} = state) do - GenServer.cast(Controller, {:file_changed, file_type(path), path}) - - {:noreply, %{state | debounce_timers: Map.delete(debounce_timers, path)}} + def handle_info(:throttle_timer_complete, state) do + {:noreply, %{state | throttling: false}} end def handle_info({:file_event, watcher_pid, :stop}, %{watcher_pid: watcher_pid} = state) do