Skip to content

Commit

Permalink
Fix bugs, increase coverage, update benchmark results
Browse files Browse the repository at this point in the history
Fixes #1187
  • Loading branch information
szmarczak committed Apr 30, 2020
1 parent e9359d3 commit b927e2d
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 43 deletions.
24 changes: 12 additions & 12 deletions benchmark/index.ts
Expand Up @@ -183,18 +183,18 @@ const internalBenchmark = (): void => {
};

// Results (i7-7700k, CPU governor: performance):
// got - promise x 3,092 ops/sec 卤5.25% (73 runs sampled)
// got - stream x 4,313 ops/sec 卤5.61% (72 runs sampled)
// got - promise core x 6,756 ops/sec 卤5.32% (80 runs sampled)
// got - stream core x 6,863 ops/sec 卤4.68% (76 runs sampled)
// got - stream core - normalized options x 7,960 ops/sec 卤3.83% (81 runs sampled)
// request - callback x 6,912 ops/sec 卤6.50% (76 runs sampled)
// request - stream x 7,821 ops/sec 卤4.28% (80 runs sampled)
// node-fetch - promise x 7,036 ops/sec 卤8.17% (78 runs sampled)
// node-fetch - stream x 7,877 ops/sec 卤4.17% (80 runs sampled)
// axios - promise x 6,613 ops/sec 卤3.22% (76 runs sampled)
// axios - stream x 8,642 ops/sec 卤2.84% (79 runs sampled)
// https - stream x 9,955 ops/sec 卤6.36% (76 runs sampled)
// got - promise x 3,204 ops/sec 卤5.27% (73 runs sampled)
// got - stream x 5,045 ops/sec 卤3.85% (77 runs sampled)
// got - promise core x 6,499 ops/sec 卤3.67% (77 runs sampled)

This comment has been minimized.

Copy link
@szmarczak

szmarczak Apr 30, 2020

Author Collaborator

We can speed up the promise core by merging PromisableRequest into Request. Performance of extended classes is quite slow.

// new X()
// 35,588,657 ops/s 卤0.99%
class X {
	constructor(options = {}) {
    	this.options = this.constructor.normalize(options);
    }
  
  	static normalize(options) {
    	return {...options};
    }
}

// new Y()
// 4,850,624 ops/s 卤1%
class Y extends X {
  	static normalize(options) {
      	const normalized = super.normalize(options);
      
      	normalized.on = Boolean(normalized.on);
    	
      	return normalized;
    }
}

This comment has been minimized.

Copy link
@szmarczak

szmarczak Apr 30, 2020

Author Collaborator

I think this function is responsible for slowing Got down. But haven't checked that yet.

got/source/create.ts

Lines 113 to 161 in b927e2d

const got: Got = ((url: string | URL, options?: Options): GotReturn => {
let iteration = 0;
const iterateHandlers = (newOptions: NormalizedOptions): GotReturn => {
return defaults.handlers[iteration++](
newOptions,
iteration === defaults.handlers.length ? getPromiseOrStream : iterateHandlers
) as GotReturn;
};
if (is.plainObject(url)) {
options = {
...url as Options,
...options
};
url = undefined as any;
}
try {
// Call `init` hooks
let initHookError: Error | undefined;
try {
callInitHooks(defaults.options.hooks.init, options);
callInitHooks(options?.hooks?.init, options);
} catch (error) {
initHookError = error;
}
// Normalize options & call handlers
const normalizedOptions = normalizeArguments(url, options, defaults.options);
normalizedOptions[kIsNormalizedAlready] = true;
if (initHookError) {
throw new RequestError(initHookError.message, initHookError, normalizedOptions);
}
// A bug.
// eslint-disable-next-line @typescript-eslint/return-await
return iterateHandlers(normalizedOptions);
} catch (error) {
if (options?.isStream) {
throw error;
} else {
// A bug.
// eslint-disable-next-line @typescript-eslint/return-await
return createRejection(error, defaults.options.hooks.beforeError, options?.hooks?.beforeError);
}
}
}) as Got;

// got - stream core x 7,047 ops/sec 卤2.32% (83 runs sampled)
// got - stream core - normalized options x 7,313 ops/sec 卤2.79% (85 runs sampled)
// request - callback x 6,918 ops/sec 卤5.76% (73 runs sampled)
// request - stream x 7,746 ops/sec 卤3.20% (82 runs sampled)
// node-fetch - promise x 7,011 ops/sec 卤7.54% (75 runs sampled)
// node-fetch - stream x 7,941 ops/sec 卤4.52% (82 runs sampled)
// axios - promise x 6,788 ops/sec 卤3.32% (80 runs sampled)
// axios - stream x 8,584 ops/sec 卤2.23% (81 runs sampled)
// https - stream x 10,465 ops/sec 卤2.89% (73 runs sampled)
// Fastest is https - stream

// got - normalize options x 166,389 ops/sec 卤0.63% (91 runs sampled)
10 changes: 4 additions & 6 deletions source/as-promise/core.ts
Expand Up @@ -33,12 +33,10 @@ export const parseBody = (response: Response, responseType: ResponseType, encodi
return Buffer.from(rawBody);
}

if (!knownBodyTypes.includes(responseType)) {
throw new ParseError({
message: `Unknown body type '${responseType as string}'`,
name: 'Error'
}, response);
}
throw new ParseError({
message: `Unknown body type '${responseType as string}'`,
name: 'Error'
}, response);
} catch (error) {
throw new ParseError(error, response);
}
Expand Down
16 changes: 5 additions & 11 deletions source/as-promise/index.ts
Expand Up @@ -7,8 +7,7 @@ import {
CancelableRequest,
Response,
RequestError,
HTTPError,
ReadError
HTTPError
} from './types';
import PromisableRequest, {parseBody} from './core';
import proxyEvents from '../core/utils/proxy-events';
Expand Down Expand Up @@ -80,9 +79,9 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ
rawBody = await getStream.buffer(request);

response.rawBody = rawBody;
} catch (error) {
// TODO: Call `request._beforeError`, see https://github.com/nodejs/node/issues/32995
reject(new ReadError(error, request));
} catch (_) {
// The same error is caught below.
// See request.once('error')
return;
}

Expand Down Expand Up @@ -135,12 +134,7 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ
}
} catch (error) {
// TODO: Call `request._beforeError`, see https://github.com/nodejs/node/issues/32995
if (error instanceof RequestError) {
reject(error);
} else {
reject(new RequestError(error.message, error, request));
}

reject(new RequestError(error.message, error, request));
return;
}

Expand Down
22 changes: 11 additions & 11 deletions source/core/index.ts
Expand Up @@ -469,7 +469,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {

declare options: NormalizedOptions;
declare requestUrl: string;
finalized: boolean;
requestInitialized: boolean;
redirects: string[];

constructor(url: string | URL, options: Options = {}, defaults?: Defaults) {
Expand All @@ -480,7 +480,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {

this[kDownloadedSize] = 0;
this[kUploadedSize] = 0;
this.finalized = false;
this.requestInitialized = false;
this[kServerResponsesPiped] = new Set<ServerResponse>();
this.redirects = [];
this[kStopReading] = false;
Expand Down Expand Up @@ -558,7 +558,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
job();
}

this.finalized = true;
this.requestInitialized = true;
} catch (error) {
if (error instanceof RequestError) {
this._beforeError(error);
Expand Down Expand Up @@ -674,11 +674,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
options.searchParams = new URLSearchParams(options.searchParams as Record<string, string>);

// `normalizeArguments()` is also used to merge options
if (defaults?.searchParams) {
defaults.searchParams.forEach((value, key) => {
(options!.searchParams as URLSearchParams).append(key, value);
});
}
defaults?.searchParams?.forEach((value, key) => {
(options!.searchParams as URLSearchParams).append(key, value);
});

This comment has been minimized.

Copy link
@jaulz

jaulz Apr 30, 2020

Contributor

Do we really need to merge the search params? While using the pagination API I noticed that we cannot simply pass a new url in the options nor set clean searchParams.

} else {
options.searchParams = defaults?.searchParams;
}
Expand Down Expand Up @@ -992,7 +990,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
this.emit('downloadProgress', this.downloadProgress);
});

response.on('error', (error: Error) => {
response.once('error', (error: Error) => {
this._beforeError(new ReadError(error, this));
});

Expand Down Expand Up @@ -1415,7 +1413,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
this._writeRequest(chunk, encoding, callback);
};

if (this.finalized) {
if (this.requestInitialized) {
write();
} else {
this[kJobs].push(write);
Expand Down Expand Up @@ -1470,7 +1468,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
});
};

if (this.finalized) {
if (this.requestInitialized) {
endRequest();
} else {
this[kJobs].push(endRequest);
Expand All @@ -1479,6 +1477,8 @@ export default class Request extends Duplex implements RequestEvents<Request> {

_destroy(error: Error | null, callback: (error: Error | null) => void): void {
if (kRequest in this) {
this[kCancelTimeouts]!();

// TODO: Remove the next `if` when these get fixed:
// - https://github.com/nodejs/node/issues/32851
// - https://github.com/nock/nock/issues/1981
Expand Down
1 change: 1 addition & 0 deletions source/core/utils/timed-out.ts
Expand Up @@ -83,6 +83,7 @@ export default (request: ClientRequest, delays: Delays, options: TimedOutOptions
cancelTimeouts();

// Save original behavior
/* istanbul ignore next */
if (request.listenerCount('error') === 0) {
throw error;
}
Expand Down
6 changes: 3 additions & 3 deletions source/create.ts
Expand Up @@ -73,10 +73,10 @@ const aliases: readonly HTTPAlias[] = [

export const defaultHandler: HandlerFunction = (options, next) => next(options);

const callInitHooks = (hooks: InitHook[] | undefined, options: Options): void => {
const callInitHooks = (hooks: InitHook[] | undefined, options?: Options): void => {
if (hooks) {
for (const hook of hooks) {
hook(options);
hook(options!);
}
}
};
Expand Down Expand Up @@ -110,7 +110,7 @@ const create = (defaults: InstanceDefaults): Got => {
return result;
}));

const got: Got = ((url: string | URL, options: Options = {}): GotReturn => {
const got: Got = ((url: string | URL, options?: Options): GotReturn => {
let iteration = 0;
const iterateHandlers = (newOptions: NormalizedOptions): GotReturn => {
return defaults.handlers[iteration++](
Expand Down
9 changes: 9 additions & 0 deletions test/arguments.ts
Expand Up @@ -477,6 +477,15 @@ test('normalizes search params included in input', t => {
t.is(url.search, '?a=b+c');
});

test('normalizes search params included in options', t => {
const {url} = got.mergeOptions({
url: new URL('https://example.com'),
searchParams: 'a=b c'
});

t.is(url.search, '?a=b+c');
});

test('reuse options while using init hook', withServer, async (t, server, got) => {
t.plan(2);

Expand Down
41 changes: 41 additions & 0 deletions test/cancel.ts
Expand Up @@ -2,6 +2,7 @@ import {EventEmitter} from 'events';
import {Readable as ReadableStream} from 'stream';
import stream = require('stream');
import test from 'ava';
import delay = require('delay');
import pEvent = require('p-event');
import getStream = require('get-stream');
import {Handler} from 'express';
Expand Down Expand Up @@ -75,6 +76,31 @@ test.serial('does not retry after cancelation', withServerAndLolex, async (t, se
await t.notThrowsAsync(promise, 'Request finished instead of aborting.');
});

test.serial('cleans up request timeouts', withServer, async (t, server, got) => {
server.get('/', () => {});

const gotPromise = got('redirect', {
timeout: 10,
retry: {
calculateDelay: ({computedValue}) => {
process.nextTick(() => gotPromise.cancel());

if (computedValue) {
return 20;
}

return 0;
},
limit: 1
}
});

await t.throwsAsync(gotPromise, {instanceOf: CancelError});

// Wait for unhandled errors
await delay(40);
});

test.serial('cancels in-progress request', withServerAndLolex, async (t, server, got, clock) => {
const {emitter, promise} = prepareServer(server, clock);

Expand Down Expand Up @@ -219,3 +245,18 @@ test('throws when canceling cached request', withServer, async (t, server, got)

await t.throwsAsync(promise, {instanceOf: got.CancelError});
});

test('throws when canceling cached request #2', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.setHeader('Cache-Control', 'public, max-age=60');
response.end(Date.now().toString());
});

const cache = new Map();
await got({cache});

const promise = got({cache});
promise.cancel();

await t.throwsAsync(promise, {instanceOf: got.CancelError});
});
2 changes: 2 additions & 0 deletions test/stream.ts
Expand Up @@ -379,11 +379,13 @@ test('the socket is alive on a successful pipeline', withServer, async (t, serve
});

const gotStream = got.stream('');
t.is(gotStream.socket, undefined);

const receiver = new stream.PassThrough();
await promisify(stream.pipeline)(gotStream, receiver);

t.is(await getStream(receiver), payload);
t.truthy(gotStream.socket);
t.false(gotStream.socket!.destroyed);
});

Expand Down
25 changes: 25 additions & 0 deletions test/timeout.ts
Expand Up @@ -455,6 +455,31 @@ test.serial('no unhandled timeout errors', withServer, async (t, _server, got) =
await delay(200);
});

// TODO: use lolex here
test.serial('no unhandled timeout errors #2', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.write('Hello world!');
});

const gotPromise = got('', {
timeout: 20,
retry: {
calculateDelay: ({computedValue}) => {
if (computedValue) {
return 10;
}

return 0;
},
limit: 1
}
});

await t.throwsAsync(gotPromise, {instanceOf: TimeoutError});

await delay(100);
});

test.serial('no more timeouts after an error', withServer, async (t, _server, got) => {
const {setTimeout} = global;
const {clearTimeout} = global;
Expand Down
22 changes: 22 additions & 0 deletions test/weakable-map.ts
@@ -0,0 +1,22 @@
import test from 'ava';
import WeakableMap from '../source/core/utils/weakable-map';

test('works as expected', t => {
const weakable = new WeakableMap();

weakable.set('hello', 'world');

t.true(weakable.has('hello'));
t.false(weakable.has('foobar'));
t.is(weakable.get('hello'), 'world');
t.is(weakable.get('foobar'), undefined);

const object = {};
const anotherObject = {};
weakable.set(object, 'world');

t.true(weakable.has(object));
t.false(weakable.has(anotherObject));
t.is(weakable.get(object), 'world');
t.is(weakable.get(anotherObject), undefined);
});

0 comments on commit b927e2d

Please sign in to comment.