Skip to content

Commit 120b04e

Browse files
fcollonvalianhi
andauthored
Reduce polling (#812)
* Clarify refresh workflow model refresh -> emit signals -> update view This renders the history tab coherent * Refresh when tab is shown * Clarify link between headChanged and refreshBranch Co-authored-by: Ian Hunt-Isaak <[email protected]>
1 parent b5dc2d5 commit 120b04e

File tree

8 files changed

+141
-87
lines changed

8 files changed

+141
-87
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Once installed, extension behavior can be modified via the following settings wh
4747
- **displayStatus**: display Git extension status updates in the JupyterLab status bar. If `true`, the extension displays status updates in the JupyterLab status bar, such as when pulling and pushing changes, switching branches, and polling for changes. Depending on the level of extension activity, some users may find the status updates distracting. In which case, setting this to `false` should reduce visual noise.
4848
- **doubleClickDiff**: double click a file in the Git extension panel to open a diff of the file instead of opening the file for editing.
4949
- **historyCount**: number of commits shown in the history log, beginning with the most recent. Displaying a larger number of commits can lead to performance degradation, so use caution when modifying this setting.
50+
- **refreshIfHidden**: whether to refresh even if the Git tab is hidden; default to `false` (i.e. refresh is turned off if the Git tab is hidden).
5051
- **refreshInterval**: number of milliseconds between polling the file system for changes. In order to ensure that the UI correctly displays the current repository status, the extension must poll the file system for changes. Longer polling times increase the likelihood that the UI does not reflect the current status; however, longer polling times also incur less performance overhead.
5152
- **simpleStaging**: enable a simplified concept of staging. When this setting is `true`, all files with changes are automatically staged. When we develop in JupyterLab, we often only care about what files have changed (in the broadest sense) and don't need to distinguish between "tracked" and "untracked" files. Accordingly, this setting allows us to simplify the visual presentation of changes, which is especially useful for those less acquainted with Git.
5253

schema/plugin.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
"description": "Number of (most recent) commits shown in the history log",
4242
"default": 25
4343
},
44+
"refreshIfHidden": {
45+
"type": "boolean",
46+
"title": "Refresh if the Git tab is hidden",
47+
"description": "Whether to check Git status when the Git tab is not visible. Choose `false` for higher performance.",
48+
"default": false
49+
},
4450
"refreshInterval": {
4551
"type": "integer",
4652
"title": "Refresh interval",

src/components/GitPanel.tsx

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
124124
this.setState({
125125
repository: args.newValue
126126
});
127-
this.refresh();
127+
this.refreshView();
128128
}, this);
129129
model.statusChanged.connect(() => {
130130
this.setState({ files: model.status });
@@ -133,13 +133,11 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
133133
await this.refreshBranch();
134134
if (this.state.tab === 1) {
135135
this.refreshHistory();
136-
} else {
137-
this.refreshStatus();
138136
}
139137
}, this);
140138
model.markChanged.connect(() => this.forceUpdate());
141139

142-
settings.changed.connect(this.refresh, this);
140+
settings.changed.connect(this.refreshView, this);
143141
}
144142

145143
refreshBranch = async () => {
@@ -168,18 +166,13 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
168166
}
169167
};
170168

171-
refreshStatus = async () => {
172-
await this.props.model.refreshStatus();
173-
};
174-
175169
/**
176170
* Refresh widget, update all content
177171
*/
178-
refresh = async () => {
172+
refreshView = async () => {
179173
if (this.props.model.pathRepository !== null) {
180174
await this.refreshBranch();
181175
await this.refreshHistory();
182-
await this.refreshStatus();
183176
}
184177
};
185178

@@ -279,7 +272,6 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
279272
commands={this.props.commands}
280273
logger={this.props.logger}
281274
model={this.props.model}
282-
refresh={this._onRefresh}
283275
repository={this.state.repository || ''}
284276
/>
285277
);
@@ -446,20 +438,6 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
446438
});
447439
};
448440

449-
/**
450-
* Callback invoked upon refreshing a repository.
451-
*
452-
* @returns promise which refreshes a repository
453-
*/
454-
private _onRefresh = async () => {
455-
await this.refreshBranch();
456-
if (this.state.tab === 1) {
457-
this.refreshHistory();
458-
} else {
459-
this.refreshStatus();
460-
}
461-
};
462-
463441
/**
464442
* Determines whether a user has a known Git identity.
465443
*

src/components/Toolbar.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,6 @@ export interface IToolbarProps {
7373
* Current repository.
7474
*/
7575
repository: string;
76-
77-
/**
78-
* Callback to invoke in order to refresh a repository.
79-
*
80-
* @returns promise which refreshes a repository
81-
*/
82-
refresh: () => Promise<void>;
8376
}
8477

8578
/**
@@ -320,18 +313,18 @@ export class Toolbar extends React.Component<IToolbarProps, IToolbarState> {
320313
};
321314

322315
/**
323-
* Callback invoked upon clicking a button to refresh a repository.
316+
* Callback invoked upon clicking a button to refresh the model.
324317
*
325318
* @param event - event object
326-
* @returns a promise which resolves upon refreshing a repository
319+
* @returns a promise which resolves upon refreshing the model
327320
*/
328321
private _onRefreshClick = async (): Promise<void> => {
329322
this.props.logger.log({
330323
level: Level.RUNNING,
331324
message: 'Refreshing...'
332325
});
333326
try {
334-
await this.props.refresh();
327+
await this.props.model.refresh();
335328

336329
this.props.logger.log({
337330
level: Level.SUCCESS,

src/model.ts

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -39,35 +39,19 @@ export class GitExtension implements IGitExtension {
3939
let interval: number;
4040
if (settings) {
4141
interval = settings.composite.refreshInterval as number;
42-
settings.changed.connect(onSettingsChange, this);
42+
settings.changed.connect(this._onSettingsChange, this);
4343
} else {
4444
interval = DEFAULT_REFRESH_INTERVAL;
4545
}
46-
const poll = new Poll({
47-
factory: () => this.refresh(),
46+
this._poll = new Poll({
47+
factory: this._refreshModel,
4848
frequency: {
4949
interval: interval,
5050
backoff: true,
5151
max: 300 * 1000
5252
},
53-
standby: 'when-hidden'
53+
standby: this._refreshStandby
5454
});
55-
this._poll = poll;
56-
57-
/**
58-
* Callback invoked upon a change to plugin settings.
59-
*
60-
* @private
61-
* @param settings - settings registry
62-
*/
63-
function onSettingsChange(settings: ISettingRegistry.ISettings) {
64-
const freq = poll.frequency;
65-
poll.frequency = {
66-
interval: settings.composite.refreshInterval as number,
67-
backoff: freq.backoff,
68-
max: freq.max
69-
};
70-
}
7155
}
7256

7357
/**
@@ -152,6 +136,16 @@ export class GitExtension implements IGitExtension {
152136
}
153137
}
154138

139+
/**
140+
* Custom model refresh standby condition
141+
*/
142+
get refreshStandbyCondition(): () => boolean {
143+
return this._standbyCondition;
144+
}
145+
set refreshStandbyCondition(v: () => boolean) {
146+
this._standbyCondition = v;
147+
}
148+
155149
/**
156150
* A list of modified files.
157151
*
@@ -401,7 +395,6 @@ export class GitExtension implements IGitExtension {
401395

402396
if (body.checkout_branch) {
403397
await this.refreshBranch();
404-
this._headChanged.emit();
405398
} else {
406399
await this.refreshStatus();
407400
}
@@ -454,8 +447,7 @@ export class GitExtension implements IGitExtension {
454447
top_repo_path: path
455448
});
456449
});
457-
await this.refreshStatus();
458-
this._headChanged.emit();
450+
await this.refresh();
459451
}
460452

461453
/**
@@ -675,7 +667,7 @@ export class GitExtension implements IGitExtension {
675667
});
676668
}
677669
);
678-
this._headChanged.emit();
670+
this.refreshBranch(); // Will emit headChanged if required
679671
return data;
680672
}
681673

@@ -700,7 +692,7 @@ export class GitExtension implements IGitExtension {
700692
});
701693
}
702694
);
703-
this._headChanged.emit();
695+
this.refreshBranch();
704696
return data;
705697
}
706698

@@ -710,15 +702,15 @@ export class GitExtension implements IGitExtension {
710702
* @returns promise which resolves upon refreshing the repository
711703
*/
712704
async refresh(): Promise<void> {
713-
await this._taskHandler.execute<void>('git:refresh', async () => {
714-
await this.refreshBranch();
715-
await this.refreshStatus();
716-
});
705+
await this._poll.refresh();
706+
await this._poll.tick;
717707
}
718708

719709
/**
720710
* Refresh the list of repository branches.
721711
*
712+
* Emit headChanged if the branch or its top commit changes
713+
*
722714
* @returns promise which resolves upon refreshing repository branches
723715
*/
724716
async refreshBranch(): Promise<void> {
@@ -730,7 +722,15 @@ export class GitExtension implements IGitExtension {
730722
}
731723
);
732724

733-
const headChanged = this._currentBranch !== data.current_branch;
725+
let headChanged = false;
726+
if (!this._currentBranch || !data) {
727+
headChanged = this._currentBranch !== data.current_branch; // Object comparison is not working
728+
} else {
729+
headChanged =
730+
this._currentBranch.name !== data.current_branch.name ||
731+
this._currentBranch.top_commit !== data.current_branch.top_commit;
732+
}
733+
734734
this._branches = data.branches;
735735
this._currentBranch = data.current_branch;
736736
if (this._currentBranch) {
@@ -758,6 +758,8 @@ export class GitExtension implements IGitExtension {
758758
/**
759759
* Refresh the repository status.
760760
*
761+
* Emit statusChanged if required.
762+
*
761763
* @returns promise which resolves upon refreshing the repository status
762764
*/
763765
async refreshStatus(): Promise<void> {
@@ -865,7 +867,6 @@ export class GitExtension implements IGitExtension {
865867
});
866868
});
867869
await this.refreshBranch();
868-
this._headChanged.emit();
869870
}
870871

871872
/**
@@ -1143,12 +1144,25 @@ export class GitExtension implements IGitExtension {
11431144
this._statusChanged.emit(this._status);
11441145
}
11451146

1147+
/**
1148+
* Callback invoked upon a change to plugin settings.
1149+
*
1150+
* @private
1151+
* @param settings - plugin settings
1152+
*/
1153+
private _onSettingsChange(settings: ISettingRegistry.ISettings) {
1154+
this._poll.frequency = {
1155+
...this._poll.frequency,
1156+
interval: settings.composite.refreshInterval as number
1157+
};
1158+
}
1159+
11461160
/**
11471161
* open new editor or show an existing editor of the
11481162
* .gitignore file. If the editor does not have unsaved changes
11491163
* then ensure the editor's content matches the file on disk
11501164
*/
1151-
private _openGitignore() {
1165+
private _openGitignore(): void {
11521166
if (this._docmanager) {
11531167
const widget = this._docmanager.openOrReveal(
11541168
this.getRelativeFilePath('.gitignore')
@@ -1159,6 +1173,38 @@ export class GitExtension implements IGitExtension {
11591173
}
11601174
}
11611175

1176+
/**
1177+
* Refresh model status through a Poll
1178+
*/
1179+
private _refreshModel = async (): Promise<void> => {
1180+
await this._taskHandler.execute<void>('git:refresh', async () => {
1181+
try {
1182+
await this.refreshBranch();
1183+
await this.refreshStatus();
1184+
} catch (error) {
1185+
console.error('Failed to refresh git status', error);
1186+
}
1187+
});
1188+
};
1189+
1190+
/**
1191+
* Standby test function for the refresh Poll
1192+
*
1193+
* Standby refresh if
1194+
* - webpage is hidden
1195+
* - not in a git repository
1196+
* - standby condition is true
1197+
*
1198+
* @returns The test function
1199+
*/
1200+
private _refreshStandby = (): boolean | Poll.Standby => {
1201+
if (this.pathRepository === null || this._standbyCondition()) {
1202+
return true;
1203+
}
1204+
1205+
return 'when-hidden';
1206+
};
1207+
11621208
/**
11631209
* if file is open in JupyterLab find the widget and ensure the JupyterLab
11641210
* version matches the version on disk. Do nothing if the file has unsaved changes
@@ -1194,6 +1240,7 @@ export class GitExtension implements IGitExtension {
11941240
private _pendingReadyPromise = 0;
11951241
private _poll: Poll;
11961242
private _settings: ISettingRegistry.ISettings | null;
1243+
private _standbyCondition: () => boolean = () => false;
11971244
private _taskHandler: TaskHandler<IGitExtension>;
11981245

11991246
private _headChanged = new Signal<IGitExtension, void>(this);

src/tokens.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ export interface IGitExtension extends IDisposable {
4848
*/
4949
ready: Promise<void>;
5050

51+
/**
52+
* Custom model refresh standby condition
53+
*/
54+
refreshStandbyCondition: () => boolean;
55+
5156
/**
5257
* Files list resulting of a Git status call.
5358
*/

0 commit comments

Comments
 (0)