Unverified Commit 6ecb2aad authored by Benjamin Chodoroff's avatar Benjamin Chodoroff Committed by GitHub
Browse files

add request metrics to server-side render, and refactor (#2814)

remove signup bonus calculation

refactor SSR:

- clientside render
- url rewriting
- get state from api

refactor universalRender into client and server fns
(this makes requirements more clear)

add RequestTimer and StatsLoggerClient classes for request timing logging to statsd

new env var: STATSD_IP

note the TODO about tags -- they do not work yet in scalyr / statsd
parent b1e16d49
......@@ -6,7 +6,7 @@ import { VIEW_MODE_WHISTLE, PARAM_VIEW_MODE } from 'shared/constants';
import './assets/stylesheets/app.scss';
import plugins from 'app/utils/JsPlugins';
import Iso from 'iso';
import universalRender from 'shared/UniversalRender';
import { clientRender } from 'shared/UniversalRender';
import ConsoleExports from './utils/ConsoleExports';
import { serverApiRecordEvent } from 'app/utils/ServerApiClient';
import * as steem from '@steemit/steem-js';
......@@ -112,10 +112,13 @@ function runApp(initial_state) {
const location = `${window.location.pathname}${window.location.search}${
window.location.hash
}`;
universalRender({ history, location, initial_state }).catch(error => {
try {
clientRender(initial_state);
} catch (error) {
console.error(error);
serverApiRecordEvent('client_error', error);
});
}
}
if (!window.Intl) {
......
......@@ -2,7 +2,7 @@ import React from 'react';
import { renderToString } from 'react-dom/server';
import { VIEW_MODE_WHISTLE, PARAM_VIEW_MODE } from '../shared/constants';
import ServerHTML from './server-html';
import universalRender from '../shared/UniversalRender';
import { serverRender } from '../shared/UniversalRender';
import models from 'db/models';
import secureRandom from 'secure-random';
import ErrorPage from 'server/server-error';
......@@ -27,6 +27,8 @@ function getSupportedLocales() {
const supportedLocales = getSupportedLocales();
async function appRender(ctx) {
ctx.state.requestTimer.startTimer('appRender_ms');
const store = {};
// This is the part of SSR where we make session-specific changes:
......@@ -80,14 +82,14 @@ async function appRender(ctx) {
},
};
const { body, title, statusCode, meta } = await universalRender({
const { body, title, statusCode, meta } = await serverRender(
ctx.request.url,
initial_state,
location: ctx.request.url,
store,
ErrorPage,
userPreferences,
offchain,
});
ctx.state.requestTimer
);
// Assets name are found in `webpack-stats` file
const assets_filename =
......@@ -119,6 +121,8 @@ async function appRender(ctx) {
throw err;
}
ctx.state.requestTimer.stopTimer('appRender_ms');
}
appRender.dbStatus = { ok: true };
......
function requestTime(numProcesses) {
let number_of_requests = 0;
import RequestTimer from './utils/RequestTimer';
function requestTime(statsLoggerClient) {
return function*(next) {
number_of_requests += 1;
const start = Date.now();
this.state.requestTimer = new RequestTimer(
statsLoggerClient,
'request',
`method=${this.request.method} path=${this.request.path}`
);
yield* next;
const delta = Math.ceil(Date.now() - start);
// log all requests that take longer than 150ms
if (delta > 150)
console.log(
`Request took too long! ${delta}ms: ${this.request.method} ${
this.request.path
}. Number of parallel requests: ${
number_of_requests
}, number of processes: ${numProcesses}`
);
number_of_requests -= 1;
this.state.requestTimer.finish();
};
}
......
......@@ -5,6 +5,7 @@ import mount from 'koa-mount';
import helmet from 'koa-helmet';
import koa_logger from 'koa-logger';
import requestTime from './requesttimings';
import StatsLoggerClient from './utils/StatsLoggerClient';
import hardwareStats from './hardwarestats';
import cluster from 'cluster';
import os from 'os';
......@@ -84,7 +85,9 @@ app.use(isBot());
// (unless passed in as an env var)
const numProcesses = process.env.NUM_PROCESSES || os.cpus().length;
app.use(requestTime(numProcesses));
const statsLoggerClient = new StatsLoggerClient(process.env.STATSD_IP);
app.use(requestTime(statsLoggerClient));
app.keys = [config.get('session_key')];
......
import assert from 'assert';
import StatsLoggerClient from './StatsLoggerClient';
/**
* @param {array} hrtime process.hrtime() tuple
* @returns {number} nanoseconds
*/
const hrtimeToNanoseconds = hrtime => +hrtime[0] * 1e9 + +hrtime[1];
/**
* @param {array} hrtime process.hrtime() tuple
* @returns {number} milliseconds
*/
const hrtimeToMilliseconds = hrtime => +hrtime[0] * 1000 + +hrtime[1] / 1000000;
/**
* Logs total request time starting at instantiation and ending when finish() is called.
* Additional timers can be managed with startTimer('name') and stopTimer('name')
*
* Results are stored in `timers` property and submitted to statsd at finish().
*/
export default class RequestTimer {
/**
*
* @param {StatsLoggerClient} statsLoggerClient
* @param {string} prefix namespace to tack on the front of each timer name
* @param {string} tags not yet supported by statsd / StatsLoggerClient
*/
constructor(statsLoggerClient, prefix, tags) {
assert(
statsLoggerClient instanceof StatsLoggerClient,
'provide an instance of StatsLoggerClient'
);
this.start = process.hrtime();
this.timers = [];
this.inProgressTimers = {};
this.prefix = prefix;
this.requestTags = tags;
this.statsLoggerClient = statsLoggerClient;
}
/**
* @param {string} name
* @param {number} duration milliseconds
*/
logSegment(name, duration) {
this.timers.push([`${this.prefix}.${name}`, duration]);
}
/**
* Starts keeping track of something to time.
*
* @param {string} name
*/
startTimer(name) {
assert(
typeof name === 'string',
'a name for the timer must be provided'
);
this.inProgressTimers[name] = process.hrtime();
}
/**
* Stops an in-progress timer and stores it in the list of timers to log when the request is finished.
*
* @param {*} name
*/
stopTimer(name) {
assert(
typeof this.inProgressTimers[name] !== 'undefined',
'provide an existing timer name'
);
this.logSegment(
name,
hrtimeToMilliseconds(process.hrtime(this.inProgressTimers[name]))
);
delete this.inProgressTimers[name];
}
finish() {
this.logSegment(
'total_ms',
hrtimeToMilliseconds(process.hrtime(this.start))
);
this.statsLoggerClient.logTimers(this.timers, this.requestTags);
}
}
import SDC from 'statsd-client';
/**
* In production, log stats to statsd.
* In dev, console.log 'em.
*/
export default class StatsLoggerClient {
constructor(STATSD_IP) {
if (STATSD_IP) {
this.SDC = new SDC({
host: STATSD_IP,
prefix: 'condenser',
});
} else {
console.log(
'StatsLoggerClient: no server available, logging to console.'
);
// Implement debug loggers here, as any new calls are added in methods below.
this.SDC = {
timing() {
console.log('StatsLoggerClient call: ', arguments);
},
};
}
}
/**
* Given an array of timer tuples that look like [namespace, value]
* log them all to statsd.
*/
logTimers(tuples) {
const timestamp = +new Date();
tuples.map(tuple => {
this.SDC.timing(tuple[0], tuple[1]);
});
}
}
......@@ -225,14 +225,26 @@ const onRouterError = error => {
console.error('onRouterError', error);
};
async function universalRender({
/**
*
* @param {*} location
* @param {*} initialState
* @param {*} ErrorPage
* @param {*} userPreferences
* @param {*} offchain
* @param {RequestTimer} requestTimer
* @returns promise
*/
export async function serverRender(
location,
initial_state,
offchain,
initialState,
ErrorPage,
userPreferences,
}) {
offchain,
requestTimer
) {
let error, redirect, renderProps;
try {
[error, redirect, renderProps] = await runRouter(location, RootRoute);
} catch (e) {
......@@ -245,6 +257,7 @@ async function universalRender({
),
};
}
if (error || !renderProps) {
// debug('error')('Router error', error);
return {
......@@ -254,81 +267,16 @@ async function universalRender({
};
}
if (process.env.BROWSER) {
const store = createStore(rootReducer, initial_state, middleware);
const history = syncHistoryWithStore(browserHistory, store);
/**
* When to scroll - on hash link navigation determine if the page should scroll to that element (forward nav, or ignore nav direction)
*/
const scroll = useScroll({
createScrollBehavior: config => new OffsetScrollBehavior(config), //information assembler for has scrolling.
shouldUpdateScroll: (prevLocation, { location }) => {
// eslint-disable-line no-shadow
//if there is a hash, we may want to scroll to it
if (location.hash) {
//if disableNavDirectionCheck exists, we want to always navigate to the hash (the page is telling us that's desired behavior based on the element's existence
const disableNavDirectionCheck = document.getElementById(
DISABLE_ROUTER_HISTORY_NAV_DIRECTION_EL_ID
);
//we want to navigate to the corresponding id=<hash> element on 'PUSH' navigation (prev null + POP is a new window url nav ~= 'PUSH')
if (
disableNavDirectionCheck ||
(prevLocation === null && location.action === 'POP') ||
location.action === 'PUSH'
) {
return location.hash;
}
}
return true;
},
});
if (process.env.NODE_ENV === 'production') {
console.log(
'%c%s',
'color: red; background: yellow; font-size: 24px;',
'WARNING!'
);
console.log(
'%c%s',
'color: black; font-size: 16px;',
'This is a developer console, you must read and understand anything you paste or type here or you could compromise your account and your private keys.'
);
}
return render(
<Provider store={store}>
<Translator>
<Router
routes={RootRoute}
history={history}
onError={onRouterError}
render={applyRouterMiddleware(scroll)}
/>
</Translator>
</Provider>,
document.getElementById('content')
);
}
// below is only executed on the server
let server_store, onchain;
try {
let url = location === '/' ? 'trending' : location;
// Replace /curation-rewards and /author-rewards with /transfers for UserProfile
// to resolve data correctly
if (url.indexOf('/curation-rewards') !== -1)
url = url.replace(/\/curation-rewards$/, '/transfers');
if (url.indexOf('/author-rewards') !== -1)
url = url.replace(/\/author-rewards$/, '/transfers');
if (process.env.OFFLINE_SSR_TEST) {
onchain = get_state_perf;
} else {
onchain = await api.getStateAsync(url);
}
const url = getUrlFromLocation(location);
requestTimer.startTimer('apiGetState_ms');
onchain = await apiGetState(url);
requestTimer.stopTimer('apiGetState_ms');
// If a user profile URL is requested but no profile information is
// included in the API response, return User Not Found.
if (
Object.getOwnPropertyNames(onchain.accounts).length === 0 &&
(url.match(routeRegex.UserProfile1) ||
......@@ -354,6 +302,7 @@ async function universalRender({
}
}
// Are we loading an un-category-aliased post?
if (
!url.match(routeRegex.PostsIndex) &&
!url.match(routeRegex.UserProfile1) &&
......@@ -379,24 +328,9 @@ async function universalRender({
};
}
}
// Calculate signup bonus
const fee = parseFloat($STM_Config.registrar_fee.split(' ')[0]),
{ base, quote } = onchain.feed_price,
feed =
parseFloat(base.split(' ')[0]) /
parseFloat(quote.split(' ')[0]);
const sd = fee * feed;
let sdDisp;
if (sd < 1.0) {
sdDisp = '¢' + parseInt(sd * 100);
} else {
const sdInt = parseInt(sd),
sdDec = sd - sdInt;
sdDisp = '$' + sdInt + (sdInt < 5 && sdDec >= 0.5 ? '.50' : '');
}
server_store = createStore(rootReducer, {
app: initial_state.app,
app: initialState.app,
global: onchain,
offchain,
});
......@@ -429,6 +363,7 @@ async function universalRender({
let app, status, meta;
try {
requestTimer.startTimer('ssr_ms');
app = renderToString(
<Provider store={server_store}>
<Translator>
......@@ -436,6 +371,7 @@ async function universalRender({
</Translator>
</Provider>
);
requestTimer.stopTimer('ssr_ms');
meta = extractMeta(onchain, renderProps.params);
status = 200;
} catch (re) {
......@@ -453,4 +389,101 @@ async function universalRender({
};
}
export default universalRender;
/**
* dependencies:
* middleware
* browserHistory
* useScroll
* OffsetScrollBehavior
* location
*
* @param {*} initialState
*/
export function clientRender(initialState) {
const store = createStore(rootReducer, initialState, middleware);
const history = syncHistoryWithStore(browserHistory, store);
/**
* When to scroll - on hash link navigation determine if the page should scroll to that element (forward nav, or ignore nav direction)
*/
const scroll = useScroll({
createScrollBehavior: config => new OffsetScrollBehavior(config), //information assembler for has scrolling.
shouldUpdateScroll: (prevLocation, { location }) => {
// eslint-disable-line no-shadow
//if there is a hash, we may want to scroll to it
if (location.hash) {
//if disableNavDirectionCheck exists, we want to always navigate to the hash (the page is telling us that's desired behavior based on the element's existence
const disableNavDirectionCheck = document.getElementById(
DISABLE_ROUTER_HISTORY_NAV_DIRECTION_EL_ID
);
//we want to navigate to the corresponding id=<hash> element on 'PUSH' navigation (prev null + POP is a new window url nav ~= 'PUSH')
if (
disableNavDirectionCheck ||
(prevLocation === null && location.action === 'POP') ||
location.action === 'PUSH'
) {
return location.hash;
}
}
return true;
},
});
if (process.env.NODE_ENV === 'production') {
console.log(
'%c%s',
'color: red; background: yellow; font-size: 24px;',
'WARNING!'
);
console.log(
'%c%s',
'color: black; font-size: 16px;',
'This is a developer console, you must read and understand anything you paste or type here or you could compromise your account and your private keys.'
);
}
return render(
<Provider store={store}>
<Translator>
<Router
routes={RootRoute}
history={history}
onError={onRouterError}
render={applyRouterMiddleware(scroll)}
/>
</Translator>
</Provider>,
document.getElementById('content')
);
}
/**
* Do some pre-state-fetch url rewriting.
*
* @param {string} location
* @returns {string}
*/
function getUrlFromLocation(location) {
let url = location === '/' ? 'trending' : location;
// Replace /curation-rewards and /author-rewards with /transfers for UserProfile
// to resolve data correctly
if (url.indexOf('/curation-rewards') !== -1)
url = url.replace(/\/curation-rewards$/, '/transfers');
if (url.indexOf('/author-rewards') !== -1)
url = url.replace(/\/author-rewards$/, '/transfers');
return url;
}
async function apiGetState(url) {
let offchain;
if (process.env.OFFLINE_SSR_TEST) {
offchain = get_state_perf;
}
offchain = await api.getStateAsync(url);
return offchain;
}
......@@ -8678,6 +8678,10 @@ staged-git-files@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35"
statsd-client@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/statsd-client/-/statsd-client-0.4.2.tgz#c0dc4d583609f97d638742f3ee412c34e9aea482"
statuses@1, "statuses@>= 1.3.1 < 2", statuses@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment