Fix race condition when logging the runtime of a process.

`ProcessRunner.TimedProcessRunnerObserver` can be used concurrently by multiple threads. Therefore, we need to serialize access to its `pendingLogHandles` dictionary.

We use `os_unfair_lock` to serialize access because it is efficient when contention is rare, which should be the case in this situation.

PiperOrigin-RevId: 304271197
diff --git a/src/TulsiGenerator/ProcessRunner.swift b/src/TulsiGenerator/ProcessRunner.swift
index 8e8fc0a..9607ad1 100644
--- a/src/TulsiGenerator/ProcessRunner.swift
+++ b/src/TulsiGenerator/ProcessRunner.swift
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import Darwin
 import Foundation
 
-
 /// Encapsulates functionality to launch and manage Processes.
 public final class ProcessRunner {
 
@@ -42,13 +42,42 @@
     private static var KVOContext: Int = 0
 
     /// Mapping between Processes and LogSessionHandles created for each.
+    ///
+    /// - Warning: Do not access directly; use `accessPendingLogHandles` instead.
+    /// - SeeAlso: `accessPendingLogHandles`
     private var pendingLogHandles = Dictionary<Process, LocalizedMessageLogger.LogSessionHandle>()
 
+    /// Lock to serialize access to `pendingLogHandles`.
+    ///
+    /// We use `os_unfair_lock` because it is efficient when contention is rare, which should be
+    /// the case in this situation.
+    private var pendingLogHandlesLock = os_unfair_lock()
+
+    /// Provides thread-safe access to `pendingLogHandles`.
+    ///
+    /// We need to access `pendingLogHandles` in a thread-safe way because
+    /// `TimedProcessRunnerObserver` can be used concurrently by multiple threads.
+    ///
+    /// - Parameter usePendingLogHandles: The critical section that reads from or writes to
+    ///                                   `pendingLogHandles`. Minimize the code in the critical
+    ///                                   section to avoid unnecessary lock contention.
+    private func accessPendingLogHandles<T>(usePendingLogHandles: (inout Dictionary<Process, LocalizedMessageLogger.LogSessionHandle>) throws -> T) rethrows -> T {
+      os_unfair_lock_lock(&pendingLogHandlesLock)
+      defer {
+        os_unfair_lock_unlock(&pendingLogHandlesLock)
+      }
+
+      return try usePendingLogHandles(&pendingLogHandles)
+    }
+
     /// Start logging the given Process with KVO to determine the time when it starts running.
     fileprivate func startLoggingProcessTime(process: Process,
                                              loggingIdentifier: String,
                                              messageLogger: LocalizedMessageLogger) {
-      self.pendingLogHandles[process] = messageLogger.startProfiling(loggingIdentifier)
+      let logSessionHandle = messageLogger.startProfiling(loggingIdentifier)
+      accessPendingLogHandles { pendingLogHandles in
+        pendingLogHandles[process] = logSessionHandle
+      }
       process.addObserver(self,
                           forKeyPath: #keyPath(Process.isRunning),
                           options: .new,
@@ -62,7 +91,9 @@
         process.removeObserver(self,
                                forKeyPath: #keyPath(Process.isRunning),
                                context: &TimedProcessRunnerObserver.KVOContext)
-        self.pendingLogHandles.removeValue(forKey: process)
+        accessPendingLogHandles { pendingLogHandles in
+          _ = pendingLogHandles.removeValue(forKey: process)
+        }
       }
     }
 
@@ -80,7 +111,9 @@
           let newValue = change?[NSKeyValueChangeKey.newKey] as? NSNumber,
           newValue.boolValue,
           let process = object as? Process {
-        pendingLogHandles[process]?.resetStartTime()
+        accessPendingLogHandles { pendingLogHandles in
+          pendingLogHandles[process]?.resetStartTime()
+        }
       }
     }
   }