Skip to content

Commit

Permalink
Progressive batch rendering (#270)
Browse files Browse the repository at this point in the history
* setup initial websocket communication

* websockets integrated into rendering

* added completion event

* removed dead code; fixed race condition in websockets; fix bug in sending data to a closed ws

* v bump

* cleaned up code
  • Loading branch information
Anthony Dresser authored Nov 10, 2016
1 parent 2620053 commit 8ff3899
Show file tree
Hide file tree
Showing 13 changed files with 397 additions and 313 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
"tmp": "^0.0.28",
"underscore": "^1.8.3",
"vscode-extension-telemetry": "^0.0.5",
"vscode-languageclient": "^2.5.0"
"vscode-languageclient": "^2.5.0",
"ws": "^1.1.1"
},
"contributes": {
"languages": [
Expand Down
2 changes: 1 addition & 1 deletion src/configurations/dev.config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"service": {
"downloadUrl": "http://dtnuget:8080/download/microsoft.sqltools.servicelayer/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "0.0.35",
"version": "0.0.37",
"downloadFileNames": {
"Windows": "win-x64-netcoreapp1.0.zip",
"OSX": "osx-x64-netcoreapp1.0.tar.gz",
Expand Down
18 changes: 14 additions & 4 deletions src/controllers/QueryNotificationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
*/
import QueryRunner from './QueryRunner';
import SqlToolsServiceClient from '../languageservice/serviceclient';
import {QueryExecuteCompleteNotification} from '../models/contracts/queryExecute';
import {NotificationHandler} from 'vscode-languageclient';
import { QueryExecuteCompleteNotification,
QueryExecuteBatchCompleteNotification } from '../models/contracts/queryExecute';
import { NotificationHandler } from 'vscode-languageclient';

export class QueryNotificationHandler {
private static _instance: QueryNotificationHandler;
Expand All @@ -23,7 +24,8 @@ export class QueryNotificationHandler {

// register the handler to handle notifications for queries
private initialize(): void {
SqlToolsServiceClient.instance.onNotification(QueryExecuteCompleteNotification.type, this.handleNotification());
SqlToolsServiceClient.instance.onNotification(QueryExecuteCompleteNotification.type, this.handleCompleteNotification());
SqlToolsServiceClient.instance.onNotification(QueryExecuteBatchCompleteNotification.type, this.handleBatchCompleteNotification());
}

// registers queryRunners with their uris to distribute notifications
Expand All @@ -32,11 +34,19 @@ export class QueryNotificationHandler {
}

// handles distributing notifications to appropriate
private handleNotification(): NotificationHandler<any> {
private handleCompleteNotification(): NotificationHandler<any> {
const self = this;
return (event) => {
self._queryRunners.get(event.ownerUri).handleResult(event);
self._queryRunners.delete(event.ownerUri);
};
}

// handles distributing notifications to appropriate
private handleBatchCompleteNotification(): NotificationHandler<any> {
const self = this;
return (event) => {
self._queryRunners.get(event.ownerUri).handleBatchResult(event);
};
}
}
21 changes: 17 additions & 4 deletions src/controllers/QueryRunner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
import { EventEmitter } from 'events';

import StatusView from '../views/statusView';
import SqlToolsServerClient from '../languageservice/serviceclient';
Expand All @@ -7,7 +8,7 @@ import VscodeWrapper from './vscodeWrapper';
import { BatchSummary, QueryExecuteParams, QueryExecuteRequest,
QueryExecuteCompleteNotificationResult, QueryExecuteSubsetResult,
QueryExecuteSubsetParams, QueryDisposeParams, QueryExecuteSubsetRequest,
QueryDisposeRequest } from '../models/contracts/queryExecute';
QueryDisposeRequest, QueryExecuteBatchCompleteNotificationResult } from '../models/contracts/queryExecute';
import { QueryCancelParams, QueryCancelResult, QueryCancelRequest } from '../models/contracts/QueryCancel';
import { ISlickRange, ISelectionData } from '../models/interfaces';
import Constants = require('../models/constants');
Expand All @@ -25,12 +26,13 @@ export interface IResultSet {
*/
export default class QueryRunner {
// MEMBER VARIABLES ////////////////////////////////////////////////////
private _batchSets: BatchSummary[];
private _batchSets: BatchSummary[] = [];
private _isExecuting: boolean;
private _uri: string;
private _title: string;
private _resultLineOffset: number;
private _batchSetsPromise: Promise<BatchSummary[]>;
public batchResult: EventEmitter = new EventEmitter();
public dataResolveReject;

// CONSTRUCTOR /////////////////////////////////////////////////////////
Expand Down Expand Up @@ -81,11 +83,11 @@ export default class QueryRunner {
return this._batchSetsPromise;
}

private get batchSets(): BatchSummary[] {
get batchSets(): BatchSummary[] {
return this._batchSets;
}

private set batchSets(batchSets: BatchSummary[]) {
set batchSets(batchSets: BatchSummary[]) {
this._batchSets = batchSets;
}

Expand Down Expand Up @@ -174,6 +176,17 @@ export default class QueryRunner {
});
this._statusView.executedQuery(this.uri);
this.dataResolveReject.resolve(this.batchSets);
this.batchResult.emit('complete');
}

public handleBatchResult(result: QueryExecuteBatchCompleteNotificationResult): void {
let batch = result.batchSummary;
if (batch.selection) {
batch.selection.startLine = batch.selection.startLine + this._resultLineOffset;
batch.selection.endLine = batch.selection.endLine + this._resultLineOffset;
}
this._batchSets.push(batch);
this.batchResult.emit('batch', batch);
}

// get more data rows from the current resultSets from the service layer
Expand Down
33 changes: 32 additions & 1 deletion src/controllers/localWebService.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
'use strict';
import path = require('path');
import { EventEmitter } from 'events';
import * as ws from 'ws';
import url = require('url');
import querystring = require('querystring');
import Utils = require('../models/utils');
import Constants = require('../models/constants');
import Interfaces = require('../models/interfaces');
import http = require('http');
const bodyParser = require('body-parser');
const express = require('express');
const WebSocketServer = ws.Server;

export default class LocalWebService {
private app = express();
private server = http.createServer();
private wss = new WebSocketServer({ server: this.server});
private clientMap = new Map<string, ws>();
public newConnection = new EventEmitter();
static _servicePort: string;
static _vscodeExtensionPath: string;
static _htmlContentLocation = 'out/src/views/htmlcontent';
static _staticContentPath: string;

constructor(extensionPath: string) {
// add static content for express web server to serve
const self = this;
LocalWebService._vscodeExtensionPath = extensionPath;
LocalWebService._staticContentPath = path.join(extensionPath, LocalWebService._htmlContentLocation);
this.app.use(express.static(LocalWebService.staticContentPath));
this.app.use(bodyParser.json({limit: '50mb', type: 'application/json'}));
this.app.set('view engine', 'ejs');
Utils.logDebug(Constants.msgLocalWebserviceStaticContent + LocalWebService.staticContentPath);
this.server.on('request', this.app);
this.wss.on('connection', (ws) => {
let parse = querystring.parse(url.parse(ws.upgradeReq.url).query);
self.clientMap.set(parse.uri, ws);
self.newConnection.emit('connection', parse.uri);
});
}

static get serviceUrl(): string {
Expand All @@ -39,6 +56,20 @@ export default class LocalWebService {
return this.serviceUrl + '/' + Interfaces.ContentTypes[type];
}

broadcast(uri: string, event: string, data?: any): void {
let temp = {
type: event
};

if (data) {
temp['data'] = data;
}

if (this.clientMap.has(uri) && this.clientMap.get(uri).readyState === ws.OPEN) {
this.clientMap.get(uri).send(JSON.stringify(temp));
}
}

addHandler(type: Interfaces.ContentType, handler: (req, res) => void): void {
let segment = '/' + Interfaces.ContentTypes[type];
this.app.get(segment, handler);
Expand All @@ -50,7 +81,7 @@ export default class LocalWebService {
}

start(): void {
const port = this.app.listen(0).address().port; // 0 = listen on a random port
const port = this.server.listen(0).address().port; // 0 = listen on a random port
Utils.logDebug(Constants.msgLocalWebserviceStarted + port);
LocalWebService._servicePort = port.toString();
}
Expand Down
24 changes: 19 additions & 5 deletions src/models/SqlOutputContentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ export class SqlOutputContentProvider implements vscode.TextDocumentContentProvi

// create local express server
this._service = new LocalWebService(context.extensionPath);
this._service.newConnection.on('connection', (uri) => {
if (self._queryResultsMap.has(uri)) {
for (let batch of self._queryResultsMap.get(uri).queryRunner.batchSets) {
self._service.broadcast(uri, 'batch', batch);
}

if (!self._queryResultsMap.get(uri).queryRunner.isExecutingQuery) {
self._service.broadcast(uri, 'complete');
}
}
});

// add http handler for '/root'
this._service.addHandler(Interfaces.ContentType.Root, (req, res): void => {
Expand All @@ -60,7 +71,7 @@ export class SqlOutputContentProvider implements vscode.TextDocumentContentProvi
let backgroundcolor: string = req.query.backgroundcolor;
let color: string = req.query.color;
let editorConfig = self._vscodeWrapper.getConfiguration('editor');
let fontfamily = editorConfig.get<string>('fontFamily');
let fontfamily = editorConfig.get<string>('fontFamily').split('\'').join('').split('"').join('');
let fontsize = editorConfig.get<number>('fontSize') + 'px';
let fontweight = editorConfig.get<string>('fontWeight');
res.render(path.join(LocalWebService.staticContentPath, Constants.msgContentProviderSqlOutputHtml),
Expand Down Expand Up @@ -276,6 +287,12 @@ export class SqlOutputContentProvider implements vscode.TextDocumentContentProvi
// We do not have a query runner for this editor, so create a new one
// and map it to the results uri
queryRunner = new QueryRunner(uri, title, statusView);
queryRunner.batchResult.on('batch', (batch) => {
this._service.broadcast(resultsUri, 'batch', batch);
});
queryRunner.batchResult.on('complete', () => {
this._service.broadcast(resultsUri, 'complete');
});
this._queryResultsMap.set(resultsUri, new QueryRunnerState(queryRunner));
}

Expand Down Expand Up @@ -397,10 +414,7 @@ export class SqlOutputContentProvider implements vscode.TextDocumentContentProvi
"uri=${encodedUri}" +
"&theme=" + theme +
"&backgroundcolor=" + backgroundcolor +
"&color=" + color +
"&fontfamily=" + fontfamily +
"&fontweight=" + fontweight +
"&fontsize=" + fontsize;
"&color=" + color;
document.getElementById('frame').src = url;
};
</script>
Expand Down
51 changes: 34 additions & 17 deletions src/models/contracts/queryExecute.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import {RequestType, NotificationType} from 'vscode-languageclient';
import { IDbColumn, ISelectionData, IResultMessage } from './../interfaces';


export class ResultSetSummary {
id: number;
rowCount: number;
columnInfo: IDbColumn[];
}

export class BatchSummary {
hasError: boolean;
id: number;
selection: ISelectionData;
messages: IResultMessage[];
resultSetSummaries: ResultSetSummary[];
executionElapsed: string;
executionEnd: string;
executionStart: string;
}

// ------------------------------- < Query Dispose Request > ----------------------------------------
export namespace QueryDisposeRequest {
export const type: RequestType<QueryDisposeParams, QueryDisposeResult, void> = {
Expand Down Expand Up @@ -28,23 +46,6 @@ export namespace QueryExecuteCompleteNotification {
};
}

export class ResultSetSummary {
id: number;
rowCount: number;
columnInfo: IDbColumn[];
}

export class BatchSummary {
hasError: boolean;
id: number;
selection: ISelectionData;
messages: IResultMessage[];
resultSetSummaries: ResultSetSummary[];
executionElapsed: string;
executionEnd: string;
executionStart: string;
}

export class QueryExecuteCompleteNotificationResult {
ownerUri: string;
batchSummaries: BatchSummary[];
Expand All @@ -53,6 +54,22 @@ export class QueryExecuteCompleteNotificationResult {

// -------------------------- </ Query Execution Complete Notification > -------------------------------

// -------------------------- < Query Batch Complete Notification > -------------------------------
export namespace QueryExecuteBatchCompleteNotification {
export const type: NotificationType<QueryExecuteBatchCompleteNotificationResult> = {
get method(): string {
return 'query/batchComplete';
}
};
}

export class QueryExecuteBatchCompleteNotificationResult {
batchSummary: BatchSummary;
ownerUri: string;
}

// -------------------------- </ Query Batch Complete Notification > -------------------------------

// --------------------------------- < Query Execution Request > ---------------------------------------
export namespace QueryExecuteRequest {
export const type: RequestType<QueryExecuteParams, QueryExecuteResult, void> = {
Expand Down
2 changes: 2 additions & 0 deletions src/models/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ export interface IDbColumn {
isHidden?: boolean;
isIdentity?: boolean;
isKey?: boolean;
isXml?: boolean;
isJson?: boolean;
isLong?: boolean;
isReadOnly?: boolean;
isUnique?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/views/htmlcontent/src/html/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<td class="messageValue" [class.errorMessage]="imessage.hasError" style="padding-left: 20px">{{message.message}}</td>
</tr>
</template>
<tr>
<tr *ngIf="complete">
<td></td>
<td>{{Utils.formatString(Constants.elapsedTimeLabel, Utils.parseNumAsTimeString(totalElapseExecution))}}</td>
</tr>
Expand Down
Loading

0 comments on commit 8ff3899

Please sign in to comment.