Skip to content
Prev Previous commit
Next Next commit
Working with a cache
  • Loading branch information
rchiodo committed Apr 13, 2020
commit f02d847a191ae81986a5b35754294f13154dee41
2 changes: 1 addition & 1 deletion src/client/datascience/cellFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function hasCells(document: TextDocument, settings?: IDataScienceSettings
}

// CellRange is used as the basis for creating new ICells. We only use it in this file.
interface ICellRange {
export interface ICellRange {
range: Range;
title: string;
cell_type: string;
Expand Down
48 changes: 0 additions & 48 deletions src/client/datascience/editor-integration/cellhashLogger.ts

This file was deleted.

30 changes: 8 additions & 22 deletions src/client/datascience/editor-integration/cellhashprovider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ import { noop } from '../../common/utils/misc';
import { getCellResource } from '../cellFactory';
import { CellMatcher } from '../cellMatcher';
import { Identifiers } from '../constants';
import { InteractiveWindowMessages, SysInfoReason } from '../interactive-common/interactiveWindowTypes';
import {
ICell,
ICellHash,
ICellHashListener,
ICellHashProvider,
IFileHashes,
IInteractiveWindowListener
INotebookExecutionLogger
} from '../types';

interface IRangedCellHash extends ICellHash {
Expand All @@ -35,7 +34,7 @@ interface IRangedCellHash extends ICellHash {
// This class provides hashes for debugging jupyter cells. Call getHashes just before starting debugging to compute all of the
// hashes for cells.
@injectable()
export class CellHashProvider implements ICellHashProvider, IInteractiveWindowListener {
export class CellHashProvider implements ICellHashProvider, INotebookExecutionLogger {
// tslint:disable-next-line: no-any
private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{
message: string;
Expand Down Expand Up @@ -71,25 +70,6 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
return this.postEmitter.event;
}

// tslint:disable-next-line: no-any
public onMessage(message: string, payload?: any): void {
switch (message) {
case InteractiveWindowMessages.AddedSysInfo:
if (payload && payload.type) {
const reason = payload.type as SysInfoReason;
if (reason !== SysInfoReason.Interrupt) {
this.hashes.clear();
this.executionCount = 0;
this.updateEventEmitter.fire();
}
}
break;

default:
break;
}
}

public getHashes(): IFileHashes[] {
return [...this.hashes.entries()]
.map((e) => {
Expand All @@ -101,6 +81,12 @@ export class CellHashProvider implements ICellHashProvider, IInteractiveWindowLi
.filter((e) => e.hashes.length > 0);
}

public restartedKernel() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public restartedKernel() {
public onKernelRestarted() {

As we're reacting to an event that took place (i.e. this is meant to be an event handler), I think onKernelRestarted is better

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.


In reply to: 407773224 [](ancestors = 407773224)

this.hashes.clear();
this.executionCount = 0;
this.updateEventEmitter.fire();
}

public async preExecute(cell: ICell, silent: boolean): Promise<void> {
try {
if (!silent) {
Expand Down
192 changes: 137 additions & 55 deletions src/client/datascience/editor-integration/codeLensFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,37 @@
import { inject, injectable } from 'inversify';
import { CodeLens, Command, Event, EventEmitter, Range, TextDocument, Uri } from 'vscode';

import { IDocumentManager } from '../../common/application/types';
import { traceWarning } from '../../common/logger';
import { IFileSystem } from '../../common/platform/types';
import { IConfigurationService, Resource } from '../../common/types';
import * as localize from '../../common/utils/localize';
import { noop } from '../../common/utils/misc';
import { generateCellRangesFromDocument } from '../cellFactory';
import { CodeLensCommands, Commands } from '../constants';
import { generateCellRangesFromDocument, ICellRange } from '../cellFactory';
import { CodeLensCommands, Commands, Identifiers } from '../constants';
import { INotebookIdentity, InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes';
import {
ICell,
ICellHashLogger,
ICellHashProvider,
ICodeLensFactory,
IFileHashes,
IInteractiveWindowListener,
INotebook,
INotebookExecutionLogger,
INotebookProvider
} from '../types';

type CodeLensCacheData = {
cachedDocumentVersion: number | undefined;
cachedExecutionCount: number | undefined;
documentLenses: CodeLens[];
cellRanges: ICellRange[];
gotoCellLens: CodeLens[];
};

/**
* This class is a singleton that generates code lenses for any document the user opens. It listens
* to cells being execute so it can add 'goto' lenses on cells that have already been run.
*/
@injectable()
export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowListener {
private updateEvent: EventEmitter<void> = new EventEmitter<void>();
Expand All @@ -33,15 +44,21 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList
// tslint:disable-next-line: no-any
payload: any;
}>();
private cellExecutionCounts: Map<string, string> = new Map<string, string>();
private cellExecutionCounts = new Map<string, string>();
private documentExecutionCounts = new Map<string, number>();
private hashProvider: ICellHashProvider | undefined;
private notebookIdentity: Uri | undefined;
private interactiveIdentity: Uri | undefined; // Once we have more than one interactive window, this logic won't work anymore
private codeLensCache = new Map<string, CodeLensCacheData>();

constructor(
@inject(IConfigurationService) private configService: IConfigurationService,
@inject(INotebookProvider) private notebookProvider: INotebookProvider,
@inject(IFileSystem) private fileSystem: IFileSystem
) {}
@inject(IFileSystem) private fileSystem: IFileSystem,
@inject(IDocumentManager) private documentManager: IDocumentManager
) {
this.documentManager.onDidCloseTextDocument(this.onClosedDocument.bind(this));
this.configService.getSettings(undefined).onDidChange(this.onChangedSettings.bind(this));
}

public dispose(): void {
noop();
Expand All @@ -65,9 +82,15 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList
case InteractiveWindowMessages.FinishCell:
const cell = payload as ICell;
if (cell && cell.data && cell.data.execution_count) {
this.cellExecutionCounts.set(cell.id, cell.data.execution_count.toString());
if (cell.file && cell.file !== Identifiers.EmptyFileName) {
this.cellExecutionCounts.set(cell.id, cell.data.execution_count.toString());
this.documentExecutionCounts.set(
cell.file.toLocaleLowerCase(),
parseInt(cell.data.execution_count.toString(), 10)
);
this.updateEvent.fire();
}
}
this.updateEvent.fire();
break;

default:
Expand All @@ -80,61 +103,122 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList
}

public createCodeLenses(document: TextDocument): CodeLens[] {
const ranges = generateCellRangesFromDocument(
document,
this.configService.getSettings(document.uri).datascience
);
const commands = this.enumerateCommands(document.uri);
const hashes = this.configService.getSettings(document.uri).datascience.addGotoCodeLenses
? this.hashProvider
? this.hashProvider.getHashes()
: []
: [];
const codeLenses: CodeLens[] = [];
let firstCell = true;

ranges.forEach((range) => {
commands.forEach((c) => {
const codeLens = this.createCodeLens(document, range, c, firstCell);
// See if we have a cached version of the code lenses for this document
const key = document.fileName.toLocaleLowerCase();
let cache = this.codeLensCache.get(key);
let needUpdate = false;

// If we don't have one, generate one
if (!cache) {
cache = {
cachedDocumentVersion: undefined,
cachedExecutionCount: undefined,
documentLenses: [],
cellRanges: [],
gotoCellLens: []
};
needUpdate = true;
this.codeLensCache.set(key, cache);
}

// If the document version doesn't match, our cell ranges are out of date
if (cache.cachedDocumentVersion !== document.version) {
cache.cellRanges = generateCellRangesFromDocument(
document,
this.configService.getSettings(document.uri).datascience
);

// Because we have all new ranges, we need to recompute ALL of our code lenses.
cache.documentLenses = [];
cache.gotoCellLens = [];
cache.cachedDocumentVersion = document.version;
needUpdate = true;
}

// If the document execution count doesn't match, then our goto cell lens is out of date
if (cache.cachedExecutionCount !== this.documentExecutionCounts.get(key)) {
cache.gotoCellLens = [];
cache.cachedExecutionCount = this.documentExecutionCounts.get(key);
needUpdate = true;
}

// Generate our code lenses if necessary
if (cache.documentLenses.length === 0 && needUpdate && cache.cellRanges.length) {
// Enumerate the possible commands for the document based code lenses
const commands = needUpdate ? this.enumerateCommands(document.uri) : [];

// Then iterate over all of the cell ranges and generate code lenses for each possible
// commands
let firstCell = true;
cache.cellRanges.forEach((r) => {
commands.forEach((c) => {
const codeLens = this.createCodeLens(document, r, c, firstCell);
if (codeLens) {
cache?.documentLenses.push(codeLens);
}
});
firstCell = false;
});
}

// Generate the goto cell lense if necessary
if (
needUpdate &&
cache.gotoCellLens.length === 0 &&
this.hashProvider &&
cache.cellRanges.length &&
this.configService.getSettings(document.uri).datascience.addGotoCodeLenses
) {
const hashes = this.hashProvider.getHashes();
cache.cellRanges.forEach((r) => {
const codeLens = this.createExecutionLens(document, r.range, hashes);
if (codeLens) {
codeLenses.push(codeLens);
cache?.gotoCellLens.push(codeLens);
}
});
this.addExecutionCount(codeLenses, document, range.range, hashes);
firstCell = false;
});
}

return codeLenses;
return [...cache?.documentLenses, ...cache?.gotoCellLens];
}

private setIdentity(identity: INotebookIdentity) {
this.notebookIdentity = Uri.parse(identity.resource);
if (identity.type === 'interactive') {
this.interactiveIdentity = identity.resource;
}
}

private initCellHashProvider() {
this.getNotebook()
// Try getting our notebook. This should fail if
// the user hasn't opened the interactive window yet.
this.getInteractiveWindowNotebook()
.then((nb) => {
if (nb) {
const cellHashLogger = <ICellHashLogger>(
nb
.getLoggers()
.find((logger: INotebookExecutionLogger) => (<ICellHashLogger>logger).getCellHashProvider)
);

if (cellHashLogger) {
this.hashProvider = cellHashLogger.getCellHashProvider();
}
// From the notebook, find the logger that is the cell hash provider
// tslint:disable-next-line: no-any
this.hashProvider = (nb.getLoggers().find((l) => (l as any).getHashes) as any) as ICellHashProvider;
}
})
.ignoreErrors();
}

private async getNotebook(): Promise<INotebook | undefined> {
return this.notebookIdentity
? this.notebookProvider.getOrCreateNotebook({ identity: this.notebookIdentity, getOnly: true })
private async getInteractiveWindowNotebook(): Promise<INotebook | undefined> {
return this.interactiveIdentity
? this.notebookProvider.getOrCreateNotebook({ identity: this.interactiveIdentity, getOnly: true })
: undefined;
}

private onClosedDocument(doc: TextDocument) {
this.codeLensCache.delete(doc.fileName.toLocaleLowerCase());

// Don't delete the document execution count, we need to keep track
// of it past the closing of a doc if the notebook or interactive window is still open.
}

private onChangedSettings() {
// When config settings change, refresh our code lenses.
this.codeLensCache.clear();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it's on any settings change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is that .affectConfiguration function we should be using here. Or else we are clearing these out too much.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No there isn't. Not on the config settings. That's on the workspaceConfiguration.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the worst case is that the code lens are regenerated whenever you change your python settings. Meh.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can see where the configSettings uses that 'affectsConfiguration' in the configSettings.ts file (line 571 or so). It will only fire an onDidChangeSettings when the python settings change.


In reply to: 407782405 [](ancestors = 407782405)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably recompute the code lenses though. Otherwise the next execution will recompute them all.


In reply to: 407782955 [](ancestors = 407782955,407782405)

}

private enumerateCommands(resource: Resource): string[] {
let fullCommandList: string[];
// Add our non-debug commands
Expand Down Expand Up @@ -283,23 +367,21 @@ export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowList
return undefined;
}

private addExecutionCount(codeLens: CodeLens[], document: TextDocument, range: Range, hashes: IFileHashes[]) {
private createExecutionLens(document: TextDocument, range: Range, hashes: IFileHashes[]) {
const list = hashes.find((h) => this.fileSystem.arePathsSame(h.file, document.fileName));
if (list) {
// Match just the start of the range. Should be - 2 (1 for 1 based numbers and 1 for skipping the comment at the top)
const rangeMatches = list.hashes.filter((h) => h.line - 2 === range.start.line);
if (rangeMatches && rangeMatches.length) {
const rangeMatch = rangeMatches[rangeMatches.length - 1];
if (this.cellExecutionCounts.has(rangeMatch.id)) {
codeLens.push(
this.generateCodeLens(
range,
Commands.ScrollToCell,
localize.DataScience.scrollToCellTitleFormatMessage().format(
this.cellExecutionCounts.get(rangeMatch.id)!
),
[document.fileName, rangeMatch.id]
)
return this.generateCodeLens(
range,
Commands.ScrollToCell,
localize.DataScience.scrollToCellTitleFormatMessage().format(
this.cellExecutionCounts.get(rangeMatch.id)!
),
[document.fileName, rangeMatch.id]
);
}
}
Expand Down
Loading