Skip to content

Instantly share code, notes, and snippets.

@oleganza
Created October 2, 2010 08:59
Show Gist options
  • Save oleganza/607465 to your computer and use it in GitHub Desktop.
Save oleganza/607465 to your computer and use it in GitHub Desktop.
/*
In Gitbox (gitboxapp.com) there is a stage view on the right where you can see
a list of all the changes in the working directory: untracked, modified,
added, deleted, renamed files. Each change has a checkbox which you can click
to stage or unstage the change ("git add", "git reset").
When the change staging finishes, we run another task to load all the changes
("git status").
When the loading task is completed we notify the UI to update the list of changes.
All tasks are asynchronous.
The problem:
When the user quickly clicks on checkboxes we should not refresh it
multiple times. Otherwise, it will flicker with the inconsistent checkbox
states in between loading times.
Possible (non-)solutions:
1. The synchronous execution solves the problem at the expense of
slowing down the user interaction.
2. Updating UI with a delay does not solve the problem: it just makes the
updates appear later and still produces flickering when the user clicks slower.
So here is the real solution.
Let's observe possible combinations of two pairs of tasks: staging and changes' loading
when the user clicks on two checkboxes subsequently.
Abbreviations: S = staging, L = loading changes, U = UI update
Scenario 1: S2 starts before L1, so we should avoid running L1 at all.
S1----->L1----->U1
S2----->L2----->U2
Scenario 2: S2 started after L1, so we should avoid U1.
S1----->L1----->U1
S2----->L2----->U2
In both scenarios we need to know whether there are any other staging processes running or not.
If there is one, we simply avoid running loading task or at least avoid updating the UI.
This is solved using isStaging counter (named like a boolean because we don't care
about the actual number of running tasks, we care only about the fact that they are running).
isStaging is incremented before stage/unstage task begins and decremented
right after it finishes. When the task is finished and the counter is not zero, we simply avoid running next tasks.
However, there is another, more subtle scenario which I spotted only after some more testing:
Scenario 3: L1 starts before S2, but finishes after *both* S1 and S2 have finished.
S1---->L1------------>U1
S2---->L2---------->U2
In this case it is not enough to have isStaging flag. We should also ask whether there is any
loading tasks still running. For that we use isLoadingChanges counter.
After finding this scenario I tried to get away with just a single flag isStaging,
but it turned out to be impossible: if I decrement isStaging after loading is complete,
I cannot avoid starting a loading task because in scenario 1 both L1 and L2 look identical.
So without an additional flag I would have to start changes loading task each time the checkbox
is clicked, which drops the performance significantly.
In this little snippet of code we greatly improve user experience using a lot of programming patterns:
1. Grand Central Dispatch for asynchronous operations without thread management.
2. Blocks to preserve the execution context between operations and impose a strict order of events.
3. Semaphore counters for managing the stage of operations and activity indicator (popSpinning/pushSpinning).
4. Block taking a block as an argument to wrap asynchronous operations.
5. Delegation pattern to notify the UI about new data.
6. Bindings and Key-Value-Observing for blocking a checkbox when the staging is in process (aChange.busy flag).
This gives you an idea of what kind of code powers Gitbox.
This code will appear in the next update.
http://gitboxapp.com/
*/
// NSInteger isStaging; // maintains a count of the staging tasks running
// NSInteger isLoadingChanges; // maintains a count of the changes loading tasks running
// This method helps to factor out common code for both staging and unstaging tasks.
// Block declaration might look tricky, but it's just a convenient wrapper, nothing special.
// See the stage and unstage methods below.
- (void) stagingHelperForChange:(GBChange*)aChange withBlock:(void(^)(GBStage*, void(^)()))block
{
GBStage* stage = self.repository.stage;
if (aChange.busy || !stage) return;
aChange.busy = YES;
[self pushSpinning];
isStaging++;
block(stage, ^{
isStaging--;
// Avoid loading changes if another staging is running.
if (!isStaging)
{
[self pushSpinning];
isLoadingChanges++;
[stage loadChangesWithBlock:^{
isLoadingChanges--;
// Avoid publishing changes if another staging is running
// or another loading task is running.
if (!isStaging && !isLoadingChanges)
{
OAOptionalDelegateMessage(@selector(repositoryControllerDidUpdateCommitChanges:));
}
[self popSpinning];
}];
}
[self popSpinning];
});
}
// These methods are called when the user clicks a checkbox (GBChange setStaged:)
- (void) stageChange:(GBChange*)aChange
{
[self stagingHelperForChange:aChange withBlock:^(GBStage* stage, void(^block)()){
[stage stageChanges:[NSArray arrayWithObject:aChange] withBlock:block];
}];
}
- (void) unstageChange:(GBChange*)aChange
{
[self stagingHelperForChange:aChange withBlock:^(GBStage* stage, void(^block)()){
[stage unstageChanges:[NSArray arrayWithObject:aChange] withBlock:block];
}];
}
@stan1y
Copy link

stan1y commented Oct 5, 2010

Why do you need to pass a block to another block? It looks like stagingHelperForChange:withBlock & stage/unstageChanges:withBlock are both returns almost at once, so calling stage/unstageChanges:withBlock from block is redundant & it will take more time to spawn threads than to execute a block. I believe you can safely do it with single block for both.
Anyway it's pretty cool :)

@oleganza
Copy link
Author

oleganza commented Oct 6, 2010

stage/unstageChanges:withBlock calls the block asynchronously when the task has completed.

stagingHelperForChange:withBlock needs to do some common stuff before task launches and after it finishes. To do something after, it needs a block. But it does not launch the task, so it passes this block into wrapping block so it will pass it to the async task. Hence the block taking a block as an argument.

@stan1y
Copy link

stan1y commented Oct 6, 2010

Ok, no code for stage/unstageChanges:withBlock is confusing a bit.
So basically the idea is that stagingHelperForChange:withBlock does some common processing, then it calls supplied block with stage/unstage specific actions and provide common post-processing block, and then finally unstage\stageChanges:withBlock calls supplied post-processing block.
Right?

@oleganza
Copy link
Author

oleganza commented Oct 6, 2010

Exactly.

If the task was not synchronous there would be not point in the whole article: just stage the item on click and block the UI. This how the current version 0.9.7 behaves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment