From bf91d77876127b090fbb20804a82788075f67662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Thu, 11 Jul 2024 18:16:13 +0900 Subject: [PATCH] bench --- crates/swc_fast_ts_strip/benches/assets.rs | 29 ++ .../benches/assets/AjaxObservable.ts | 414 ++++++++++++++++++ 2 files changed, 443 insertions(+) create mode 100644 crates/swc_fast_ts_strip/benches/assets.rs create mode 100644 crates/swc_fast_ts_strip/benches/assets/AjaxObservable.ts diff --git a/crates/swc_fast_ts_strip/benches/assets.rs b/crates/swc_fast_ts_strip/benches/assets.rs new file mode 100644 index 000000000000..67fcee843890 --- /dev/null +++ b/crates/swc_fast_ts_strip/benches/assets.rs @@ -0,0 +1,29 @@ +use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use swc_fast_ts_strip::{operate, Options}; + +static SOURCE: &str = include_str!("assets/AjaxObservable.ts"); + +fn fast_ts(c: &mut Criterion) { + c.bench_function("typescript/fast-strip", fast_typescript); +} +fn fast_typescript(b: &mut Bencher) { + let _ = ::testing::run_test(false, |cm, handler| { + b.iter(|| { + black_box(operate( + &cm, + handler, + black_box(SOURCE.to_string()), + Options { + module: None, + filename: None, + parser: Default::default(), + }, + )) + }); + + Ok(()) + }); +} + +criterion_group!(benches, fast_ts); +criterion_main!(benches); diff --git a/crates/swc_fast_ts_strip/benches/assets/AjaxObservable.ts b/crates/swc_fast_ts_strip/benches/assets/AjaxObservable.ts new file mode 100644 index 000000000000..9019dac22bcc --- /dev/null +++ b/crates/swc_fast_ts_strip/benches/assets/AjaxObservable.ts @@ -0,0 +1,414 @@ +/** @prettier */ +import { Observable } from '../../Observable'; +import { Subscriber } from '../../Subscriber'; +import { TeardownLogic, PartialObserver } from '../../types'; + +export interface AjaxRequest { + url?: string; + body?: any; + user?: string; + async?: boolean; + method?: string; + headers?: object; + timeout?: number; + password?: string; + hasContent?: boolean; + crossDomain?: boolean; + withCredentials?: boolean; + createXHR?: () => XMLHttpRequest; + progressSubscriber?: PartialObserver; + responseType?: string; +} + +function isFormData(body: any): body is FormData { + return typeof FormData !== 'undefined' && body instanceof FormData; +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @extends {Ignored} + * @hide true + */ +export class AjaxObservable extends Observable { + private request: AjaxRequest; + + constructor(urlOrRequest: string | AjaxRequest) { + super(); + + const request: AjaxRequest = { + async: true, + createXHR: () => new XMLHttpRequest(), + crossDomain: true, + withCredentials: false, + headers: {}, + method: 'GET', + responseType: 'json', + timeout: 0, + }; + + if (typeof urlOrRequest === 'string') { + request.url = urlOrRequest; + } else { + for (const prop in urlOrRequest) { + if (urlOrRequest.hasOwnProperty(prop)) { + (request as any)[prop] = (urlOrRequest as any)[prop]; + } + } + } + + this.request = request; + } + + /** @deprecated This is an internal implementation detail, do not use. */ + _subscribe(subscriber: Subscriber): TeardownLogic { + return new AjaxSubscriber(subscriber, this.request); + } +} + +/** + * We need this JSDoc comment for affecting ESDoc. + * @ignore + * @extends {Ignored} + */ +export class AjaxSubscriber extends Subscriber { + // @ts-ignore: Property has no initializer and is not definitely assigned + private xhr: XMLHttpRequest; + private done: boolean = false; + + constructor(destination: Subscriber, public request: AjaxRequest) { + super(destination); + + const headers = (request.headers = request.headers || {}); + + // force CORS if requested + if (!request.crossDomain && !this.getHeader(headers, 'X-Requested-With')) { + (headers as any)['X-Requested-With'] = 'XMLHttpRequest'; + } + + // ensure content type is set + let contentTypeHeader = this.getHeader(headers, 'Content-Type'); + if (!contentTypeHeader && typeof request.body !== 'undefined' && !isFormData(request.body)) { + (headers as any)['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + } + + // properly serialize body + request.body = this.serializeBody(request.body, this.getHeader(request.headers, 'Content-Type')); + + this.send(); + } + + next(e: Event): void { + this.done = true; + const destination = this.destination as Subscriber; + let result: AjaxResponse; + try { + result = new AjaxResponse(e, this.xhr, this.request); + } catch (err) { + return destination.error(err); + } + destination.next(result); + } + + private send(): void { + const { + request, + request: { user, method, url, async, password, headers, body }, + } = this; + try { + const xhr = (this.xhr = request.createXHR!()); + + // set up the events before open XHR + // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest + // You need to add the event listeners before calling open() on the request. + // Otherwise the progress events will not fire. + this.setupEvents(xhr, request); + // open XHR + if (user) { + xhr.open(method!, url!, async!, user, password); + } else { + xhr.open(method!, url!, async!); + } + + // timeout, responseType and withCredentials can be set once the XHR is open + if (async) { + xhr.timeout = request.timeout!; + xhr.responseType = request.responseType as any; + } + + if ('withCredentials' in xhr) { + xhr.withCredentials = !!request.withCredentials; + } + + // set headers + this.setHeaders(xhr, headers!); + + // finally send the request + if (body) { + xhr.send(body); + } else { + xhr.send(); + } + } catch (err) { + this.error(err); + } + } + + private serializeBody(body: any, contentType?: string) { + if (!body || typeof body === 'string') { + return body; + } else if (isFormData(body)) { + return body; + } + + if (contentType) { + const splitIndex = contentType.indexOf(';'); + if (splitIndex !== -1) { + contentType = contentType.substring(0, splitIndex); + } + } + + switch (contentType) { + case 'application/x-www-form-urlencoded': + return Object.keys(body) + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(body[key])}`) + .join('&'); + case 'application/json': + return JSON.stringify(body); + default: + return body; + } + } + + private setHeaders(xhr: XMLHttpRequest, headers: Object) { + for (let key in headers) { + if (headers.hasOwnProperty(key)) { + xhr.setRequestHeader(key, (headers as any)[key]); + } + } + } + + private getHeader(headers: {}, headerName: string): any { + for (let key in headers) { + if (key.toLowerCase() === headerName.toLowerCase()) { + return (headers as any)[key]; + } + } + + return undefined; + } + + private setupEvents(xhr: XMLHttpRequest, request: AjaxRequest) { + const progressSubscriber = request.progressSubscriber; + + xhr.ontimeout = (e: ProgressEvent) => { + progressSubscriber?.error?.(e); + let error; + try { + error = new AjaxTimeoutError(xhr, request); // TODO: Make better. + } catch (err) { + error = err; + } + this.error(error); + }; + + if (progressSubscriber) { + xhr.upload.onprogress = (e: ProgressEvent) => { + progressSubscriber.next?.(e); + }; + } + + xhr.onerror = (e: ProgressEvent) => { + progressSubscriber?.error?.(e); + this.error(new AjaxError('ajax error', xhr, request)); + }; + + xhr.onload = (e: ProgressEvent) => { + // 4xx and 5xx should error (https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) + if (xhr.status < 400) { + progressSubscriber?.complete?.(); + this.next(e); + this.complete(); + } else { + progressSubscriber?.error?.(e); + let error; + try { + error = new AjaxError('ajax error ' + xhr.status, xhr, request); + } catch (err) { + error = err; + } + this.error(error); + } + }; + } + + unsubscribe() { + const { done, xhr } = this; + if (!done && xhr && xhr.readyState !== 4 && typeof xhr.abort === 'function') { + xhr.abort(); + } + super.unsubscribe(); + } +} + +/** + * A normalized AJAX response. + * + * @see {@link ajax} + * + * @class AjaxResponse + */ +export class AjaxResponse { + /** @type {number} The HTTP status code */ + status: number; + + /** @type {string|ArrayBuffer|Document|object|any} The response data */ + response: any; + + /** @type {string} The raw responseText */ + // @ts-ignore: Property has no initializer and is not definitely assigned + responseText: string; + + /** @type {string} The responseType (e.g. 'json', 'arraybuffer', or 'xml') */ + responseType: string; + + constructor(public originalEvent: Event, public xhr: XMLHttpRequest, public request: AjaxRequest) { + this.status = xhr.status; + this.responseType = xhr.responseType || request.responseType!; + this.response = getXHRResponse(xhr); + } +} + +export type AjaxErrorNames = 'AjaxError' | 'AjaxTimeoutError'; + +/** + * A normalized AJAX error. + * + * @see {@link ajax} + * + * @class AjaxError + */ +export interface AjaxError extends Error { + /** + * The XHR instance associated with the error + */ + xhr: XMLHttpRequest; + + /** + * The AjaxRequest associated with the error + */ + request: AjaxRequest; + + /** + *The HTTP status code + */ + status: number; + + /** + *The responseType (e.g. 'json', 'arraybuffer', or 'xml') + */ + responseType: XMLHttpRequestResponseType; + + /** + * The response data + */ + response: any; +} + +export interface AjaxErrorCtor { + /** + * Internal use only. Do not manually create instances of this type. + * @internal + */ + new(message: string, xhr: XMLHttpRequest, request: AjaxRequest): AjaxError; +} + +const AjaxErrorImpl = (() => { + function AjaxErrorImpl(this: any, message: string, xhr: XMLHttpRequest, request: AjaxRequest): AjaxError { + Error.call(this); + this.message = message; + this.name = 'AjaxError'; + this.xhr = xhr; + this.request = request; + this.status = xhr.status; + this.responseType = xhr.responseType; + let response: any; + try { + response = getXHRResponse(xhr); + } catch (err) { + response = xhr.responseText; + } + this.response = response; + return this; + } + AjaxErrorImpl.prototype = Object.create(Error.prototype); + return AjaxErrorImpl; +})(); + +/** + * Thrown when an error occurs during an AJAX request. + * This is only exported because it is useful for checking to see if an error + * is an `instanceof AjaxError`. DO NOT create new instances of `AjaxError` with + * the constructor. + * + * @class AjaxError + * @see ajax + */ +export const AjaxError: AjaxErrorCtor = AjaxErrorImpl as any; + +function getXHRResponse(xhr: XMLHttpRequest) { + switch (xhr.responseType) { + case 'json': { + if ('response' in xhr) { + return xhr.response; + } else { + // IE + const ieXHR: any = xhr; + return JSON.parse(ieXHR.responseText); + } + } + case 'document': + return xhr.responseXML; + case 'text': + default: { + if ('response' in xhr) { + return xhr.response; + } else { + // IE + const ieXHR: any = xhr; + return ieXHR.responseText; + } + } + } +} + +export interface AjaxTimeoutError extends AjaxError { } + +export interface AjaxTimeoutErrorCtor { + /** + * Internal use only. Do not manually create instances of this type. + * @internal + */ + new(xhr: XMLHttpRequest, request: AjaxRequest): AjaxTimeoutError; +} + +const AjaxTimeoutErrorImpl = (() => { + function AjaxTimeoutErrorImpl(this: any, xhr: XMLHttpRequest, request: AjaxRequest) { + AjaxError.call(this, 'ajax timeout', xhr, request); + this.name = 'AjaxTimeoutError'; + return this; + } + AjaxTimeoutErrorImpl.prototype = Object.create(AjaxError.prototype); + return AjaxTimeoutErrorImpl; +})(); + +/** + * Thrown when an AJAX request timeout. Not to be confused with {@link TimeoutError}. + * + * This is exported only because it is useful for checking to see if errors are an + * `instanceof AjaxTimeoutError`. DO NOT use the constructor to create an instance of + * this type. + * + * @class AjaxTimeoutError + * @see ajax + */ +export const AjaxTimeoutError: AjaxTimeoutErrorCtor = AjaxTimeoutErrorImpl as any;