Skip to content

Commit

Permalink
Implemented Reminder system in Grace; removed use of Dapr reminders f…
Browse files Browse the repository at this point in the history
…or Grace actor reminders; implemented a first-draft timing check for drilling down on perf issues; other minor performance improvements.
  • Loading branch information
ScottArbeit committed Dec 12, 2024
1 parent f481544 commit 4043124
Show file tree
Hide file tree
Showing 72 changed files with 3,003 additions and 1,321 deletions.
12 changes: 9 additions & 3 deletions docs/What grace watch does.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,15 @@ Of course, it's open-source, please feel free to examine [Watch.CLI.fs](https://
- When a promotion event from your parent branch is sent to `grace watch` by the server, `grace watch` will run auto-rebase.
- Every 4.8 minutes, `grace watch` will recompute and rewrite the Grace interprocess-communication (IPC) file, which requires reading and deserializing the local Grace Status file. The size of the IPC file is under 1K for small repos, and scales with the number of directories in the repo. A repo with 275 directories would fit in a 10K IPC file, and a repo with 2,750 directories would fit in a 100K IPC file. They're usually very small.
> Long story about why we rewrite the file: Imagine that you're at the command line, and you run `grace checkpoint -m ...`. That instance of Grace uses the existence of the IPC file as proof that `grace watch` is running in a separate process. `grace watch` writes the IPC file as soon as it starts, and, deletes it in a `try...finally` clause when it exits. In other words: in any normal exit, including exits caused by unhandled exceptions, the IPC file will be deleted when `grace watch` exits. However: it's possible that `grace watch` could be killed before it has a chance to execute that `finally` clause. For instance, in Windows, if I open Task Manager, right-click on the `grace watch` process, and hit `End Task`, the process dies immediately, and does not execute the `finally` clause. To ensure that there's not a stale IPC file laying around, Grace checks the value of the UpdatedAt field; if it's more than 5 minutes old, Grace will ignore the IPC file and assume that `grace watch` isn't running. So: _that's_ why the IPC file gets refreshed every 4.8 minutes: it resets the UpdatedAt field so the file stays under 5 minutes old.
- Once a minute, `grace watch` does a full garbage collection: `GC.Collect(2, GCCollectionMode.Forced, blocking = true, compacting = true)`. This ensures that `grace watch` keeps the smallest possible memory footprint.
- Once a minute, `grace watch` does the fullest of garbage collection:

> The .NET Runtime has excellent heuristics for when to run, but the biggest factor is memory pressure. If the OS isn't signaling that there's memory pressure on the system, GC's don't happen much. With `grace watch` running on a developer box with GB's of RAM available, it's likely that there won't be much memory pressure, and it would rarely run a GC. It would look like it's taking up a lot of memory (from doing things like auto-upload, auto-rebase, and updating the IPC file), but it would all be Gen 0 references, ready to be collected. In order to be the best possible citizen of your computer, `grace watch` proactively releases that memory by requesting GC's. Under 1ms.
`GC.Collect(2, GCCollectionMode.Forced, blocking = true, compacting = true)`

This ensures that `grace watch` keeps the smallest possible memory footprint, and takes single-digit µs when there's nothing to collect.

> The .NET Runtime has excellent heuristics for when to run GC, but the biggest factor is memory pressure. If the OS isn't signaling that there's memory pressure on the system, GC's don't happen much. With `grace watch` running on a developer box with many GB's of RAM available, it's likely that there won't be much memory pressure, and it would rarely perform garbage collection. `grace watch` might look like it's taking up a lot of memory (from doing things like auto-upload, auto-rebase, and updating the IPC file), but it would all be Gen 0 references, ready to be collected.
>
> Given that there will be many times that a user isn't working in a repository, releasing memory proactively is the right thing to do.
## Process Monitor: `grace watch` is very quiet
This is a Windows-specific story, but it illustrates what `grace watch` is doing, or _not_ doing, regardless of platform. Those of you familiar with Windows administration will be familiar with [Sysinternals Tools](https://learn.microsoft.com/en-us/sysinternals/), originally written by, and still partially maintained by, Microsoft Azure CTO Mark Russinovich. Before he was the CTO and Chief Architect of Azure, he was the Chief Architect of Windows, and he literally wrote the book _Windows Internals_, which is a great read if you're an OS nerd of any kind. Sysinternals Tools, after 20+ years, are still essential advanced tools to know on Windows.
Expand All @@ -71,4 +77,4 @@ I can't make it any quieter than that.
### Telemetry
`grace watch` does not currently collect or send any telemetry, but I intend to before Grace ships. There will be clearly-documented ways to turn it off, if you'd prefer, and proper GDPR (and related) handling in place. The intention of this telemetry is to understand usage patterns and errors in using Grace, and to then use that data to improve both functionality and performance.

For any of you who have used a telemetry provider to understand the usage of your own app, you know what I mean. Detailed telemetry will never be kept longer than 30 days.
For any of you who have used a telemetry provider – like Datadog, or Azure Monitor, or Application Insights, or any of a hundred others – to understand the usage of your own apps, you know what I mean. Detailed telemetry will never be kept longer than 30 days.
4 changes: 2 additions & 2 deletions src/CosmosSerializer/CosmosJsonSerializer.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
Expand All @@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Core" Version="1.44.1" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.45.0-preview.1" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.46.0-preview.2" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
Expand Down
56 changes: 56 additions & 0 deletions src/Grace.Actors/ActorProxy.Extensions.Actor.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ open Grace.Actors.Extensions.MemoryCache
open Grace.Actors.Constants
open Grace.Actors.Context
open Grace.Actors.Interfaces
open Grace.Actors.Timing
open Grace.Actors.Types
open Grace.Shared
open Grace.Shared.Constants
open Grace.Shared.Types
Expand All @@ -15,21 +17,31 @@ module ActorProxy =

type Dapr.Actors.Client.IActorProxyFactory with

/// Creates a Dapr ActorProxy instance for the given interface and actor type, and adds the correlationId to the server's MemoryCache so
/// it's available in each actor's OnActivateAsync() method.
member this.CreateActorProxyWithCorrelationId<'T when 'T :> IActor>(actorId: ActorId, actorType: string, correlationId: CorrelationId) =
let actorProxy = actorProxyFactory.CreateActorProxy<'T>(actorId, actorType)
//addTiming BeforeSettingCorrelationIdInMemoryCache actorType correlationId
memoryCache.CreateCorrelationIdEntry actorId correlationId
//addTiming AfterSettingCorrelationIdInMemoryCache actorType correlationId
//logToConsole $"Created actor proxy: CorrelationId: {correlationId}; ActorType: {actorType}; ActorId: {actorId}."
actorProxy

module Branch =
/// Gets an ActorId for a Branch actor.
let GetActorId (branchId: BranchId) = ActorId($"{branchId}")

/// Creates an ActorProxy for a Branch actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (branchId: BranchId) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IBranchActor>(GetActorId branchId, ActorName.Branch, correlationId)

module BranchName =
/// Gets an ActorId for a BranchName actor.
let GetActorId (repositoryId: RepositoryId) (branchName: BranchName) = ActorId($"{branchName}|{repositoryId}")

/// Creates an ActorProxy for a BranchName actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (repositoryId: RepositoryId) (branchName: BranchName) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IBranchNameActor>(GetActorId repositoryId branchName, ActorName.BranchName, correlationId)

Expand All @@ -41,28 +53,48 @@ module ActorProxy =
else
ActorId($"{directoryId2}*{directoryId1}")

/// Creates an ActorProxy for a Diff actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (directoryId1: DirectoryVersionId) (directoryId2: DirectoryVersionId) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IDiffActor>(GetActorId directoryId1 directoryId2, ActorName.Diff, correlationId)

module DirectoryVersion =
/// Gets an ActorId for a DirectoryVersion actor.
let GetActorId (directoryVersionId: DirectoryVersionId) = ActorId($"{directoryVersionId}")

/// Creates an ActorProxy for a DirectoryVersion actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (directoryVersionId: DirectoryVersionId) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IDirectoryVersionActor>(
GetActorId directoryVersionId,
ActorName.DirectoryVersion,
correlationId
)

module GlobalLock =
/// Gets an ActorId for a GlobalLock actor.
let GetActorId (lockId: string) = ActorId($"{lockId}")

/// Creates an ActorProxy for a GlobalLock actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (lockId: string) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IGlobalLockActor>(GetActorId lockId, ActorName.GlobalLock, correlationId)

module Organization =
/// Gets an ActorId for an Organization actor.
let GetActorId (organizationId: OrganizationId) = ActorId($"{organizationId}")

/// Creates an ActorProxy for an Organization actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (organizationId: OrganizationId) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IOrganizationActor>(GetActorId organizationId, ActorName.Organization, correlationId)

module OrganizationName =
/// Gets an ActorId for an OrganizationName actor.
let GetActorId (ownerId: OwnerId) (organizationName: OrganizationName) = ActorId($"{organizationName}|{ownerId}")

/// Creates an ActorProxy for an OrganizationName actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (ownerId: OwnerId) (organizationName: OrganizationName) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IOrganizationNameActor>(
GetActorId ownerId organizationName,
Expand All @@ -71,33 +103,57 @@ module ActorProxy =
)

module Owner =
/// Gets an ActorId for an Owner actor.
let GetActorId (ownerId: OwnerId) = ActorId($"{ownerId}")

/// Creates an ActorProxy for an Owner actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (ownerId: OwnerId) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IOwnerActor>(GetActorId ownerId, ActorName.Owner, correlationId)

module OwnerName =
/// Gets an ActorId for an OwnerName actor.
let GetActorId (ownerName: OwnerName) = ActorId(ownerName)

/// Creates an ActorProxy for an OwnerName actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (ownerName: OwnerName) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IOwnerNameActor>(GetActorId ownerName, ActorName.OwnerName, correlationId)

module Reminder =
/// Gets an ActorId for a Reminder actor.
let GetActorId (reminderId: ReminderId) = ActorId($"{reminderId}")

/// Creates an ActorProxy for a Reminder actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (reminderId: ReminderId) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IReminderActor>(GetActorId reminderId, ActorName.Reminder, correlationId)

module Reference =
/// Gets an ActorId for a Reference actor.
let GetActorId (referenceId: ReferenceId) = ActorId($"{referenceId}")

/// Creates an ActorProxy for a Reference actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (referenceId: ReferenceId) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IReferenceActor>(GetActorId referenceId, ActorName.Reference, correlationId)

module Repository =
/// Gets an ActorId for a Repository actor.
let GetActorId (repositoryId: RepositoryId) = ActorId($"{repositoryId}")

/// Creates an ActorProxy for a Repository actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (repositoryId: RepositoryId) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IRepositoryActor>(GetActorId repositoryId, ActorName.Repository, correlationId)

module RepositoryName =
/// Gets an ActorId for a RepositoryName actor.
let GetActorId (ownerId: OwnerId) (organizationId: OrganizationId) (repositoryName: RepositoryName) =
ActorId($"{repositoryName}|{ownerId}|{organizationId}")

/// Creates an ActorProxy for a RepositoryName actor, and adds the correlationId to the server's MemoryCache so
/// it's available in the OnActivateAsync() method.
let CreateActorProxy (ownerId: OwnerId) (organizationId: OrganizationId) (repositoryName: RepositoryName) correlationId =
actorProxyFactory.CreateActorProxyWithCorrelationId<IRepositoryNameActor>(
GetActorId ownerId organizationId repositoryName,
Expand Down
Loading

0 comments on commit 4043124

Please sign in to comment.