1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {defer, Deferred} from '../base/deferred'; 16import {fetchWithTimeout} from '../base/http_utils'; 17import {assertExists, assertTrue} from '../base/logging'; 18import {StatusResult} from '../common/protos'; 19 20import {Engine, LoadingTracker} from './engine'; 21 22export const RPC_URL = 'http://127.0.0.1:9001/'; 23const RPC_CONNECT_TIMEOUT_MS = 2000; 24 25export interface HttpRpcState { 26 connected: boolean; 27 loadedTraceName?: string; 28 failure?: string; 29} 30 31interface QueuedRequest { 32 methodName: string; 33 reqData?: Uint8Array; 34 resp: Deferred<Uint8Array>; 35 id: number; 36} 37 38export class HttpRpcEngine extends Engine { 39 readonly id: string; 40 private nextReqId = 0; 41 private requestQueue = new Array<QueuedRequest>(); 42 private pendingRequest?: QueuedRequest = undefined; 43 errorHandler: (err: string) => void = () => {}; 44 45 constructor(id: string, loadingTracker?: LoadingTracker) { 46 super(loadingTracker); 47 this.id = id; 48 } 49 50 async parse(data: Uint8Array): Promise<void> { 51 await this.enqueueRequest('parse', data); 52 } 53 54 async notifyEof(): Promise<void> { 55 await this.enqueueRequest('notify_eof'); 56 } 57 58 async restoreInitialTables(): Promise<void> { 59 await this.enqueueRequest('restore_initial_tables'); 60 } 61 62 rawQuery(rawQueryArgs: Uint8Array): Promise<Uint8Array> { 63 return this.enqueueRequest('raw_query', rawQueryArgs); 64 } 65 66 rawComputeMetric(rawComputeMetricArgs: Uint8Array): Promise<Uint8Array> { 67 return this.enqueueRequest('compute_metric', rawComputeMetricArgs); 68 } 69 70 async enableMetatrace(): Promise<void> { 71 await this.enqueueRequest('enable_metatrace'); 72 } 73 74 disableAndReadMetatrace(): Promise<Uint8Array> { 75 return this.enqueueRequest('disable_and_read_metatrace'); 76 } 77 78 enqueueRequest(methodName: string, data?: Uint8Array): Promise<Uint8Array> { 79 const resp = defer<Uint8Array>(); 80 const req: 81 QueuedRequest = {methodName, reqData: data, resp, id: this.nextReqId++}; 82 if (this.pendingRequest === undefined) { 83 this.beginFetch(req); 84 } else { 85 this.requestQueue.push(req); 86 } 87 return resp; 88 } 89 90 private beginFetch(req: QueuedRequest) { 91 assertTrue(this.pendingRequest === undefined); 92 this.pendingRequest = req; 93 const methodName = req.methodName.toLowerCase(); 94 // Deliberately not using fetchWithTimeout() here. These queries can be 95 // arbitrarily long. 96 // Deliberately not setting cache: no-cache. Doing so invalidates also the 97 // CORS pre-flight responses, causing one OPTIONS request for each POST. 98 // no-cache is also useless because trace-processor's replies are already 99 // marked as no-cache and browsers generally already assume that POST 100 // requests are not idempotent. 101 fetch(RPC_URL + methodName, { 102 method: 'post', 103 headers: { 104 'Content-Type': 'application/x-protobuf', 105 'X-Seq-Id': `${req.id}`, // Used only for debugging. 106 }, 107 body: req.reqData || new Uint8Array(), 108 }) 109 .then(resp => this.endFetch(resp, req.id)) 110 .catch(err => this.errorHandler(err)); 111 } 112 113 private endFetch(resp: Response, expectedReqId: number) { 114 const req = assertExists(this.pendingRequest); 115 this.pendingRequest = undefined; 116 assertTrue(expectedReqId === req.id); 117 if (resp.status !== 200) { 118 req.resp.reject(`HTTP ${resp.status} - ${resp.statusText}`); 119 return; 120 } 121 resp.arrayBuffer().then(arrBuf => { 122 // Note: another request can sneak in via enqueueRequest() between the 123 // arrayBuffer() call and this continuation. At this point 124 // this.pendingRequest might be set again. 125 // If not (the most common case) submit the next queued request, if any. 126 this.maybeSubmitNextQueuedRequest(); 127 req.resp.resolve(new Uint8Array(arrBuf)); 128 }); 129 } 130 131 private maybeSubmitNextQueuedRequest() { 132 if (this.pendingRequest === undefined && this.requestQueue.length > 0) { 133 this.beginFetch(this.requestQueue.shift()!); 134 } 135 } 136 137 static async checkConnection(): Promise<HttpRpcState> { 138 const httpRpcState: HttpRpcState = {connected: false}; 139 console.info( 140 `It's safe to ignore the ERR_CONNECTION_REFUSED on ${RPC_URL} below. ` + 141 `That might happen while probing the exernal native accelerator. The ` + 142 `error is non-fatal and unlikely to be the culprit for any UI bug.`); 143 try { 144 const resp = await fetchWithTimeout( 145 RPC_URL + 'status', 146 {method: 'post', cache: 'no-cache'}, 147 RPC_CONNECT_TIMEOUT_MS); 148 if (resp.status !== 200) { 149 httpRpcState.failure = `${resp.status} - ${resp.statusText}`; 150 } else { 151 const buf = new Uint8Array(await resp.arrayBuffer()); 152 const status = StatusResult.decode(buf); 153 httpRpcState.connected = true; 154 if (status.loadedTraceName) { 155 httpRpcState.loadedTraceName = status.loadedTraceName; 156 } 157 } 158 } catch (err) { 159 httpRpcState.failure = `${err}`; 160 } 161 return httpRpcState; 162 } 163} 164