initial support for compressing responses

This commit is contained in:
Vladimir Matveev 2016-07-06 00:28:24 -07:00
parent 71a3d0a42f
commit e14a7ca0bc
9 changed files with 94 additions and 31 deletions

View File

@ -70,6 +70,10 @@ namespace Utils {
return Buffer ? Buffer.byteLength(s, encoding) : s.length;
}
export function compress(s: string): any {
return Buffer ? new Buffer(s, "utf8") : { s, length: s.length };
}
export function evalFile(fileContents: string, fileName: string, nodeContext?: any) {
const environment = getExecutionEnvironment();
switch (environment) {

View File

@ -572,6 +572,8 @@ namespace Harness.LanguageService {
this.writeMessage(message);
}
writeCompressedData() {
}
readFile(fileName: string): string {
if (fileName.indexOf(Harness.Compiler.defaultLibFileName) >= 0) {
@ -690,7 +692,8 @@ namespace Harness.LanguageService {
const server = new ts.server.Session(serverHost,
{ isCancellationRequested: () => false },
/*useOneInferredProject*/ false,
Buffer ? Buffer.byteLength : (string: string, encoding?: string) => string.length,
Utils.byteLength,
Utils.compress,
process.hrtime, serverHost);
// Fake the connection between the client and the server

View File

@ -397,6 +397,15 @@ declare namespace NodeJS {
}
}
declare namespace NodeJS {
namespace zlib {
export interface GZip {
gzipSync(buf: Buffer): Buffer;
}
export function createGZip(): GZip;
}
}
declare namespace NodeJS {
namespace fs {
interface Stats {

View File

@ -30,6 +30,8 @@ declare namespace ts.server.protocol {
* Object containing arguments for the command
*/
arguments?: any;
canCompressResponse?: boolean;
}
/**

View File

@ -4,8 +4,10 @@
/* tslint:disable:no-null-keyword */
namespace ts.server {
const readline: NodeJS.ReadLine = require("readline");
const fs: typeof NodeJS.fs = require("fs");
const zlib: typeof NodeJS.zlib = require("zlib");
const rl = readline.createInterface({
input: process.stdin,
@ -13,6 +15,11 @@ namespace ts.server {
terminal: false,
});
function compress(s: string): CompressedData {
const gzip = zlib.createGZip();
return <CompressedData><any>gzip.gzipSync(new Buffer(s, "utf8"));
}
class Logger implements ts.server.Logger {
private fd = -1;
private seq = 0;
@ -92,7 +99,7 @@ namespace ts.server {
class IOSession extends Session {
constructor(host: ServerHost, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, logger: ts.server.Logger) {
super(host, cancellationToken, useSingleInferredProject, Buffer.byteLength, process.hrtime, logger);
super(host, cancellationToken, useSingleInferredProject, Buffer.byteLength, compress, process.hrtime, logger);
}
exit() {
@ -260,15 +267,16 @@ namespace ts.server {
const pollingWatchedFileSet = createPollingWatchedFileSet();
const logger = createLoggerFromEnv();
const pending: string[] = [];
const pending: Buffer[] = [];
let canWrite = true;
function writeMessage(s: string) {
function writeMessage(buf: Buffer) {
if (!canWrite) {
pending.push(s);
pending.push(buf);
}
else {
canWrite = false;
process.stdout.write(new Buffer(s, "utf8"), setCanWriteFlagAndWriteMessageIfNecessary);
process.stdout.write(buf, setCanWriteFlagAndWriteMessageIfNecessary);
}
}
@ -279,10 +287,16 @@ namespace ts.server {
}
}
function writeCompressedData(prefix: string, compressed: CompressedData, suffix: string): void {
sys.write(prefix);
writeMessage(<Buffer><any>compressed);
sys.write(suffix);
}
const sys = <ServerHost>ts.sys;
// Override sys.write because fs.writeSync is not reliable on Node 4
sys.write = (s: string) => writeMessage(s);
sys.write = (s: string) => writeMessage(new Buffer(s, "utf8"));
sys.watchFile = (fileName, callback) => {
const watchedFile = pollingWatchedFileSet.addFile(fileName, callback);
return {
@ -294,6 +308,7 @@ namespace ts.server {
sys.clearTimeout = clearTimeout;
sys.setImmediate = setImmediate;
sys.clearImmediate = clearImmediate;
sys.writeCompressedData = writeCompressedData;
let cancellationToken: HostCancellationToken;
try {

View File

@ -10,6 +10,15 @@ namespace ts.server {
stack?: string;
}
export interface CompressedData {
__compressedDataTag: any;
length: number;
}
export interface ServerHost {
writeCompressedData(prefix: string, data: CompressedData, suffix: string): void;
}
export function generateSpaces(n: number): string {
if (!spaceCache[n]) {
let strBuilder = "";
@ -183,6 +192,7 @@ namespace ts.server {
cancellationToken: HostCancellationToken,
useSingleInferredProject: boolean,
private byteLength: (buf: string, encoding?: string) => number,
private compress: (s: string) => CompressedData,
private hrtime: (start?: number[]) => number[],
private logger: Logger) {
this.projectService =
@ -215,13 +225,27 @@ namespace ts.server {
this.host.write(line + this.host.newLine);
}
public send(msg: protocol.Message) {
private sendCompressedDataToClient(prefix: string, data: CompressedData) {
this.host.writeCompressedData(prefix, data, this.host.newLine);
}
public send(msg: protocol.Message, canCompressResponse: boolean) {
const json = JSON.stringify(msg);
if (this.logger.isVerbose()) {
this.logger.info(msg.type + ": " + json);
}
this.sendLineToClient("Content-Length: " + (1 + this.byteLength(json, "utf8")) +
"\r\n\r\n" + json);
const len = this.byteLength(json, "utf8");
if (len < 84000 || !canCompressResponse) {
this.sendLineToClient("Content-Length: " + (1 + this.byteLength(json, "utf8")) + "\r\n\r\n" + json);
}
else {
// TODO: measure time
const compressed = this.compress(json);
if (this.logger.isVerbose()) {
this.logger.info(`compressed message to ${compressed.length}`);
}
this.sendCompressedDataToClient(`Content-Length: ${compressed.length + 1}`, compressed);
}
}
public configFileDiagnosticEvent(triggerFile: string, configFile: string, diagnostics: ts.Diagnostic[]) {
@ -236,7 +260,7 @@ namespace ts.server {
diagnostics: bakedDiags
}
};
this.send(ev);
this.send(ev, /*canCompressResponse*/ false);
}
public event(info: any, eventName: string) {
@ -246,10 +270,10 @@ namespace ts.server {
event: eventName,
body: info,
};
this.send(ev);
this.send(ev, /*canCompressResponse*/ false);
}
private response(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) {
private response(info: any, cmdName: string, canCompressResponse: boolean, reqSeq = 0, errorMsg?: string) {
const res: protocol.Response = {
seq: 0,
type: "response",
@ -263,11 +287,11 @@ namespace ts.server {
else {
res.message = errorMsg;
}
this.send(res);
this.send(res, canCompressResponse);
}
public output(body: any, commandName: string, requestSequence = 0, errorMessage?: string) {
this.response(body, commandName, requestSequence, errorMessage);
public output(body: any, commandName: string, canCompressResponse: boolean, requestSequence = 0, errorMessage?: string) {
this.response(body, commandName, canCompressResponse, requestSequence, errorMessage);
}
private getLocation(position: number, scriptInfo: ScriptInfo): protocol.Location {
@ -1030,7 +1054,7 @@ namespace ts.server {
this.changeSeq++;
// make sure no changes happen before this one is finished
if (project.reloadScript(file)) {
this.output(undefined, CommandNames.Reload, reqSeq);
this.output(undefined, CommandNames.Reload, /*canCompressResponse*/ false, reqSeq);
}
}
}
@ -1407,7 +1431,7 @@ namespace ts.server {
},
[CommandNames.Configure]: (request: protocol.ConfigureRequest) => {
this.projectService.setHostConfiguration(request.arguments);
this.output(undefined, CommandNames.Configure, request.seq);
this.output(undefined, CommandNames.Configure, /*canCompressResponse*/ false, request.seq);
return this.notRequired();
},
[CommandNames.Reload]: (request: protocol.ReloadRequest) => {
@ -1473,7 +1497,7 @@ namespace ts.server {
}
else {
this.projectService.log("Unrecognized JSON command: " + JSON.stringify(request));
this.output(undefined, CommandNames.Unknown, request.seq, "Unrecognized JSON command: " + request.command);
this.output(undefined, CommandNames.Unknown, /*canCompressResponse*/ false, request.seq, "Unrecognized JSON command: " + request.command);
return { responseRequired: false };
}
}
@ -1501,22 +1525,23 @@ namespace ts.server {
this.logger.msg(leader + ": " + elapsedMs.toFixed(4).toString(), "Perf");
}
if (response) {
this.output(response, request.command, request.seq);
this.output(response, request.command, request.canCompressResponse, request.seq);
}
else if (responseRequired) {
this.output(undefined, request.command, request.seq, "No content available.");
this.output(undefined, request.command, /*canCompressResponse*/ false, request.seq, "No content available.");
}
}
catch (err) {
if (err instanceof OperationCanceledException) {
// Handle cancellation exceptions
this.output({ canceled: true }, request.command, request.seq);
this.output({ canceled: true }, request.command, /*canCompressResponse*/ false, request.seq);
return;
}
this.logError(err, message);
this.output(
undefined,
request ? request.command : CommandNames.Unknown,
/*canCompressResponse*/ false,
request ? request.seq : 0,
"Error processing request. " + (<StackTraceError>err).message + "\n" + (<StackTraceError>err).stack);
}

View File

@ -29,6 +29,9 @@ namespace ts {
writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => {
throw new Error("NYI");
},
writeCompressedData() {
throw new Error("NYI");
},
resolvePath: (path: string): string => {
throw new Error("NYI");
},

View File

@ -23,7 +23,8 @@ namespace ts.server {
setTimeout(callback, ms, ...args) { return 0; },
clearTimeout(timeoutId) { },
setImmediate: () => 0,
clearImmediate() {}
clearImmediate() {},
writeCompressedData() {}
};
const nullCancellationToken: HostCancellationToken = { isCancellationRequested: () => false };
const mockLogger: Logger = {
@ -42,7 +43,7 @@ namespace ts.server {
let lastSent: protocol.Message;
beforeEach(() => {
session = new Session(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, process.hrtime, mockLogger);
session = new Session(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.compress, process.hrtime, mockLogger);
session.send = (msg: protocol.Message) => {
lastSent = msg;
};
@ -180,7 +181,7 @@ namespace ts.server {
session.send = Session.prototype.send;
assert(session.send);
expect(session.send(msg)).to.not.exist;
expect(session.send(msg, /*canCompressResponse*/ false)).to.not.exist;
expect(lastWrittenToHost).to.equal(resultMsg);
});
});
@ -248,7 +249,7 @@ namespace ts.server {
};
const command = "test";
session.output(body, command);
session.output(body, command, /*canCompressResponse*/ false);
expect(lastSent).to.deep.equal({
seq: 0,
@ -267,7 +268,7 @@ namespace ts.server {
lastSent: protocol.Message;
customHandler = "testhandler";
constructor() {
super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, process.hrtime, mockLogger);
super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.compress, process.hrtime, mockLogger);
this.addProtocolHandler(this.customHandler, () => {
return { response: undefined, responseRequired: true };
});
@ -286,7 +287,7 @@ namespace ts.server {
};
const command = "test";
session.output(body, command);
session.output(body, command, /*canCompressResponse*/ false);
expect(session.lastSent).to.deep.equal({
seq: 0,
@ -325,7 +326,7 @@ namespace ts.server {
class InProcSession extends Session {
private queue: protocol.Request[] = [];
constructor(private client: InProcClient) {
super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, process.hrtime, mockLogger);
super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, Utils.byteLength, Utils.compress, process.hrtime, mockLogger);
this.addProtocolHandler("echo", (req: protocol.Request) => ({
response: req.arguments,
responseRequired: true
@ -346,11 +347,11 @@ namespace ts.server {
({ response } = this.executeCommand(msg));
}
catch (e) {
this.output(undefined, msg.command, msg.seq, e.toString());
this.output(undefined, msg.command, /*canCompressResponse*/ false, msg.seq, e.toString());
return;
}
if (response) {
this.output(response, msg.command, msg.seq);
this.output(response, msg.command, /*canCompressResponse*/ false, msg.seq);
}
}

View File

@ -366,6 +366,7 @@ namespace ts {
readonly getExecutingFilePath = () => this.executingFilePath;
readonly getCurrentDirectory = () => this.currentDirectory;
readonly writeFile = (path: string, content: string) => notImplemented();
readonly writeCompressedData = () => notImplemented();
readonly write = (s: string) => notImplemented();
readonly createDirectory = (s: string) => notImplemented();
readonly exit = () => notImplemented();