Distinguisher9k is a Windows system utility designed for detailed real-time monitoring and analysis of disk write activity on a per-process basis. It focuses on identifying which processes are persistently writing data to the disk, especially in the form of log files or system event logs, which can contribute to constant disk activity preventing drive idling.

Specifically, the tool addresses scenarios like the one you shared earlier, where system processes such as "System" and "svchost.exe" write continuously to various system log files and NTFS metadata files, including but not limited to:

- Windows Event Trace Log files (.etl) in directories like C:\Windows\System32\LogFiles\WMI\RtBackup\
- Local service heartbeat files like lastalive0.dat within user service profile directories
- Critical file system metadata files such as $LogFile, $Mft, $BitMap, and USN Journal ($UsnJrnl:$J), which are essential to the NTFS file system's operation
- Registry log files (e.g., SYSTEM.LOG1 and SYSTEM) in C:\Windows\System32\Config\
- Prefetch files used by Windows to optimize boot times and application loading

These constant low-level disk writes generate continuous IO activity, which can cause the physical disk to stay active without resting, impacting system performance or power management.

Distinguisher9k assists by:

- Continuously tracking disk write operations of all running processes with per-second granularity.
- Listing active processes sorted by recent write throughput to surface the highest disk consumers.
- Detecting and highlighting which files each process has open, focusing on files that are typical for logging or system tracing.
- Providing filtering mechanisms so users can quickly locate suspicious or unusually active processes by name or write volume thresholds.
- Rendering real-time charts that show the write activity trends of selected processes over approximately the last 60 seconds, helping in identifying periodic spikes or sustained logging behavior.
- Allowing direct user interventions such as terminating a selected process, exporting monitoring data to CSV files, and applying filters to focus on relevant processes.

By correlating processes with their open log files and visually presenting disk write trends, the tool empowers system administrators and power users to pinpoint potentially misbehaving or overly verbose drivers, services, or applications contributing to constant disk usage.

This is crucial for scenarios where system diagnostic logging (like ETW sessions, WMI logs, and system registry activities) combined with core OS metadata maintenance leads to persistent disk throughput that prevents HDD spin-down and increases wear or energy consumption. The tool helps identify such "chatty" processes so they can be investigated, optimized, or curtailed, resulting in better system responsiveness and potentially extended hardware life.



```

import wx
import psutil
import threading
import time
import os
import matplotlib.pyplot as plt
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import csv

# Constants for log file extensions and common directories
LOG_EXTENSIONS = ('.log', '.etl', '.evtx', '.txt')
LOG_DIRS = [
    os.path.expandvars(r'%SystemRoot%\System32\LogFiles'),
    os.path.expandvars(r'%ProgramData%'),
    os.path.expanduser(r'~\AppData\Local'),
    os.path.expanduser(r'~\AppData\Roaming'),
]

def is_log_file(filepath):
    if not filepath:
        return False
    filepath_lower = filepath.lower()
    if filepath_lower.endswith(LOG_EXTENSIONS):
        return True
    return any(dir_path.lower() in filepath_lower for dir_path in LOG_DIRS)

class DiskWriteMonitorApp(wx.Frame):
    def __init__(self):
        super().__init__(None, title="Distinguisher9k - Disk Write Monitor", size=(900, 700))
        
        self.monitoring = False
        self.process_data = {}  # pid -> {'name': str, 'write_history': [int], 'open_logs': set}
        self.selected_pid = None
        
        # Setup UI
        self.panel = wx.Panel(self)
        self.main_sizer = wx.BoxSizer(wx.VERTICAL)
        
        # Control buttons
        btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.start_btn = wx.Button(self.panel, label="Start Monitoring")
        self.stop_btn = wx.Button(self.panel, label="Stop Monitoring")
        self.stop_btn.Disable()
        self.terminate_btn = wx.Button(self.panel, label="Terminate Process")
        self.export_btn = wx.Button(self.panel, label="Export CSV")
        self.terminate_btn.Disable()
        self.export_btn.Disable()
        btn_sizer.Add(self.start_btn, 0, wx.ALL, 5)
        btn_sizer.Add(self.stop_btn, 0, wx.ALL, 5)
        btn_sizer.Add(self.terminate_btn, 0, wx.ALL, 5)
        btn_sizer.Add(self.export_btn, 0, wx.ALL, 5)
        
        # Filters
        filter_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.name_filter = wx.TextCtrl(self.panel)
        self.name_filter.SetHint("Filter by process name (case-insensitive)")
        self.throughput_filter = wx.TextCtrl(self.panel)
        self.throughput_filter.SetHint("Min write throughput KB/s (numeric)")
        filter_sizer.Add(self.name_filter, 1, wx.ALL | wx.EXPAND, 5)
        filter_sizer.Add(self.throughput_filter, 0, wx.ALL, 5)
        
        # Process list
        self.process_list = wx.ListCtrl(self.panel, style=wx.LC_REPORT | wx.BORDER_SUNKEN)
        self.process_list.InsertColumn(0, "Process Name", width=180)
        self.process_list.InsertColumn(1, "PID", width=80)
        self.process_list.InsertColumn(2, "Write KB/s", width=100)
        self.process_list.InsertColumn(3, "Open Log Files", width=450)
        
        # Chart area
        self.chart_panel = wx.Panel(self.panel)
        self.chart_fig, self.chart_ax = plt.subplots(figsize=(8, 3), dpi=100)
        self.chart_canvas = FigureCanvas(self.chart_panel, -1, self.chart_fig)
        self.chart_sizer = wx.BoxSizer(wx.VERTICAL)
        self.chart_sizer.Add(self.chart_canvas, 1, wx.EXPAND)
        self.chart_panel.SetSizer(self.chart_sizer)
        
        # Layout setup
        self.main_sizer.Add(btn_sizer, 0, wx.CENTER)
        self.main_sizer.Add(filter_sizer, 0, wx.EXPAND)
        self.main_sizer.Add(self.process_list, 1, wx.ALL | wx.EXPAND, 5)
        self.main_sizer.Add(self.chart_panel, 0, wx.ALL | wx.EXPAND, 5)
        self.panel.SetSizer(self.main_sizer)
        
        # Bind events
        self.start_btn.Bind(wx.EVT_BUTTON, self.on_start)
        self.stop_btn.Bind(wx.EVT_BUTTON, self.on_stop)
        self.terminate_btn.Bind(wx.EVT_BUTTON, self.on_terminate)
        self.export_btn.Bind(wx.EVT_BUTTON, self.on_export)
        self.process_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_process_select)
        self.name_filter.Bind(wx.EVT_TEXT, self.on_filter_change)
        self.throughput_filter.Bind(wx.EVT_TEXT, self.on_filter_change)
        
        # For monitoring thread
        self.lock = threading.Lock()
        self.monitor_thread = None
        
        # For chart data
        self.chart_histories = {}  # pid -> list of last N write rates
        
    def on_start(self, event):
        if self.monitoring:
            return
        self.monitoring = True
        self.process_data.clear()
        self.chart_histories.clear()
        self.process_list.DeleteAllItems()
        self.monitor_thread = threading.Thread(target=self.monitor_loop, daemon=True)
        self.monitor_thread.start()
        self.start_btn.Disable()
        self.stop_btn.Enable()
    
    def on_stop(self, event):
        if not self.monitoring:
            return
        self.monitoring = False
        if self.monitor_thread:
            self.monitor_thread.join()
        self.start_btn.Enable()
        self.stop_btn.Disable()
    
    def on_terminate(self, event):
        if self.selected_pid is None:
            wx.MessageBox("No process selected", "Error", wx.ICON_ERROR)
            return
        try:
            proc = psutil.Process(self.selected_pid)
            proc.terminate()
            wx.MessageBox(f"Terminated process {proc.name()} (PID {self.selected_pid})", "Success", wx.ICON_INFORMATION)
        except Exception as e:
            wx.MessageBox(f"Failed to terminate process: {e}", "Error", wx.ICON_ERROR)
    
    def on_export(self, event):
        with wx.FileDialog(self, "Save CSV report", wildcard="CSV files (*.csv)|*.csv",
                           style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog:
            if fileDialog.ShowModal() == wx.ID_CANCEL:
                return
            pathname = fileDialog.GetPath()
            try:
                with open(pathname, 'w', newline='', encoding='utf-8') as csvfile:
                    writer = csv.writer(csvfile)
                    writer.writerow(['Process Name', 'PID', 'Current Write KB/s', 'Open Log Files'])
                    with self.lock:
                        for pid, pdata in self.process_data.items():
                            write_rate = 0
                            if len(pdata['write_history']) > 0:
                                write_rate = pdata['write_history'][-1] / 1024
                            open_logs = '; '.join(pdata['open_logs']) if pdata['open_logs'] else ''
                            writer.writerow([pdata['name'], pid, f"{write_rate:.2f}", open_logs])
                wx.MessageBox(f"Report saved to {pathname}", "Success", wx.ICON_INFORMATION)
            except Exception as e:
                wx.MessageBox(f"Failed to save report: {e}", "Error", wx.ICON_ERROR)
    
    def on_process_select(self, event):
        index = event.GetIndex()
        pid = self.process_list.GetItemData(index)
        self.selected_pid = pid
        self.update_chart(pid)
    
    def on_filter_change(self, event):
        self.refresh_process_list()
    
    def monitor_loop(self):
        prev_write_bytes = {}
        while self.monitoring:
            current_data = {}
            for proc in psutil.process_iter(['pid', 'name']):
                try:
                    io_counters = proc.io_counters()
                    if io_counters is None:
                        continue
                    pid = proc.pid
                    name = proc.info['name']
                    write_bytes = io_counters.write_bytes
                    prev_bytes = prev_write_bytes.get(pid, None)
                    write_rate = 0
                    if prev_bytes is not None:
                        write_rate = write_bytes - prev_bytes
                    prev_write_bytes[pid] = write_bytes
                    
                    # Get open log files if any
                    open_logs = []
                    try:
                        files = proc.open_files()
                        for f in files:
                            if is_log_file(f.path):
                                open_logs.append(f.path)
                    except (psutil.AccessDenied, psutil.NoSuchProcess):
                        # Can't access files or process gone
                        pass
                    
                    current_data[pid] = {
                        'name': name,
                        'write_rate': write_rate,
                        'open_logs': open_logs,
                        'write_history': self.process_data.get(pid, {}).get('write_history', [])
                    }
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue
            
            with self.lock:
                # Update process_data dict and keep history capped to 60 samples (~1 min)
                for pid, pdata in current_data.items():
                    if pid not in self.process_data:
                        self.process_data[pid] = {'name': pdata['name'], 'write_history': [], 'open_logs': []}
                    self.process_data[pid]['write_history'].append(pdata['write_rate'])
                    if len(self.process_data[pid]['write_history']) > 60:
                        self.process_data[pid]['write_history'].pop(0)
                    self.process_data[pid]['open_logs'] = pdata['open_logs']
            
            wx.CallAfter(self.refresh_process_list)
            time.sleep(1)
    
    def refresh_process_list(self):
        filter_text = self.name_filter.GetValue().lower()
        try:
            thresh = float(self.throughput_filter.GetValue())
            if thresh < 0:
                thresh = 0
        except ValueError:
            thresh = 0
        
        self.process_list.Freeze()
        self.process_list.DeleteAllItems()
        row = 0
        with self.lock:
            for pid, pdata in sorted(self.process_data.items(), key=lambda x: sum(x[1]['write_history'][-1:]) if x[1]['write_history'] else 0, reverse=True):
                write_rate = pdata['write_history'][-1] if pdata['write_history'] else 0
                if write_rate < thresh:
                    continue
                if filter_text and filter_text not in pdata['name'].lower():
                    continue
                open_logs_display = ', '.join(os.path.basename(f) for f in pdata['open_logs'][:3])
                if len(pdata['open_logs']) > 3:
                    open_logs_display += ", ..."
                self.process_list.Append((pdata['name'], str(pid), f"{write_rate / 1024:.2f}", open_logs_display))
                self.process_list.SetItemData(row, pid)
                row += 1
        self.process_list.Thaw()
    
    def update_chart(self, pid):
        with self.lock:
            pdata = self.process_data.get(pid, None)
            if not pdata or not pdata['write_history']:
                self.chart_ax.clear()
                self.chart_ax.set_title("No data for selected process")
                self.chart_canvas.draw()
                return
            data = [max(0, x / 1024) for x in pdata['write_history']]  # KB/s, clamp negatives
            
        self.chart_ax.clear()
        self.chart_ax.plot(data, label=pdata['name'])
        self.chart_ax.set_ylabel("Write KB/s")
        self.chart_ax.set_xlabel("Seconds (last 60)")
        self.chart_ax.set_title(f"Disk Write Activity Over Time - PID {pid} ({pdata['name']})")
        self.chart_ax.legend()
        self.chart_canvas.draw()

def main():
    app = wx.App(False)
    frame = DiskWriteMonitorApp()
    frame.Show()
    app.MainLoop()

if __name__ == "__main__":
    main()

```


"""
```



Here is a summary of the main mistakes and errors related to the overall construct that occurred throughout this chat:

    Missing Initialization of Key Attributes
    The matplotlib components (self.chart_ax, self.chart_fig, and self.chart_canvas) were sometimes not properly initialized as instance attributes before being used in methods like clear_chart(). This caused repeated AttributeError exceptions saying these attributes did not exist.

    Incorrect Use of wx.ListCtrl Methods
    The method ScrollList was called without the required two arguments (lines and per_page), causing a TypeError about insufficient arguments.

    Handling Updates in wx.ListCtrl
    Attempts to preserve selection and scroll position during list updates sometimes led to invisible or flickering rows in the process list. Complex logic for freezing/thawing the control and restoring the UI state was prone to causing visual glitches and instability.

    Confusion Over Insertion Methods
    Using InsertItem combined with SetItem and SetItemData for list control updates brought complexity and increased the chance of off-by-one or indexing errors, compared to simpler Append with a tuple of columns which worked more reliably.

    Premature or Improper UI Updates
    Calling UI update functions that referenced uninitialized or non-existing attributes resulted in errors. Calls to redraw or refresh controls were sometimes missing or misplaced, causing invisible or incomplete UI rendering.

    Thread-Safety and UI Thread Interaction
    While mostly addressed, there remained subtle risks due to improper synchronization between monitoring threads and UI updates. Using wx.CallAfter was required but not always consistently applied in earlier versions.

    Overcomplication and Repeated Rewrites
    Multiple rewrites of the entire app introduced features and complexity faster than could be fully tested and debugged, leading to recurring problems and oscillating issues instead of incremental, stable improvements.

    Ignoring Working Foundations
    Several later versions moved away from the simpler but more stable initial working code, adding unnecessary complexities that caused regressions in UI visibility and functionality.

    Inconsistent or Missing Comments and Documentation
    The clarity of the code and explanations was sometimes sacrificed, which hampered diagnosing issues and verifying correctness.

In summary, most errors were due to improper initialization order, incorrect GUI method calls, overcomplicated UI state management, and the challenges of properly coordinating background thread updates with the wxPython event-driven UI. The stable and effective solution relied on keeping UI updates simple, initializing graphics objects upfront, and carefully updating the list control with minimal but robust logic.


```
""".