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

@@ -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);
}