Improved performance while calculating status by doing one iteration only and memoizing the result when possible

This commit is contained in:
Alejandro Celaya
2019-03-04 18:14:45 +01:00
parent 9a6fad4db5
commit 252edaa2ca
8 changed files with 118 additions and 127 deletions

View File

@@ -8,20 +8,13 @@ import DateInput from '../utils/DateInput';
import MutedMessage from '../utils/MuttedMessage';
import SortableBarGraph from './SortableBarGraph';
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
import { VisitsHeader } from './VisitsHeader';
import VisitsHeader from './VisitsHeader';
import GraphCard from './GraphCard';
import { shortUrlDetailType } from './reducers/shortUrlDetail';
import './ShortUrlVisits.scss';
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
const ShortUrlVisits = ({
processOsStats,
processBrowserStats,
processCountriesStats,
processCitiesStats,
processReferrersStats,
processCitiesStatsForMap,
}) => class ShortUrlVisits extends React.Component {
const ShortUrlVisits = ({ processStatsFromVisits }) => class ShortUrlVisits extends React.PureComponent {
static propTypes = {
match: PropTypes.shape({
params: PropTypes.object,
@@ -35,18 +28,25 @@ const ShortUrlVisits = ({
state = { startDate: undefined, endDate: undefined };
loadVisits = () => {
const { match: { params }, getShortUrlVisits } = this.props;
getShortUrlVisits(params.shortCode, mapObjIndexed(
const { shortCode } = params;
const dates = mapObjIndexed(
(value) => value && value.format ? value.format('YYYY-MM-DD') : value,
this.state
));
);
const { startDate, endDate } = dates;
// While the "page" is loaded, use the timestamp + filtering dates as memoization IDs for stats calcs
this.memoizationId = `${new Date().getTime()}_${shortCode}_${startDate}_${endDate}`;
getShortUrlVisits(shortCode, dates);
};
componentDidMount() {
const { match: { params }, getShortUrlDetail } = this.props;
const { shortCode } = params;
this.loadVisits();
getShortUrlDetail(params.shortCode);
getShortUrlDetail(shortCode);
}
render() {
@@ -71,17 +71,21 @@ const ShortUrlVisits = ({
return <MutedMessage>There are no visits matching current filter :(</MutedMessage>;
}
const { os, browsers, referrers, countries, cities, citiesForMap } = processStatsFromVisits(
{ id: this.memoizationId, visits }
);
return (
<div className="row">
<div className="col-xl-4 col-lg-6">
<GraphCard title="Operating systems" stats={processOsStats(visits)} />
<GraphCard title="Operating systems" stats={os} />
</div>
<div className="col-xl-4 col-lg-6">
<GraphCard title="Browsers" stats={processBrowserStats(visits)} />
<GraphCard title="Browsers" stats={browsers} />
</div>
<div className="col-xl-4">
<SortableBarGraph
stats={processReferrersStats(visits)}
stats={referrers}
title="Referrers"
sortingItems={{
name: 'Referrer name',
@@ -91,7 +95,7 @@ const ShortUrlVisits = ({
</div>
<div className="col-lg-6">
<SortableBarGraph
stats={processCountriesStats(visits)}
stats={countries}
title="Countries"
sortingItems={{
name: 'Country name',
@@ -101,13 +105,13 @@ const ShortUrlVisits = ({
</div>
<div className="col-lg-6">
<SortableBarGraph
stats={processCitiesStats(visits)}
stats={cities}
title="Cities"
extraHeaderContent={[
() => (
<OpenMapModalBtn
modalTitle="Cities"
locations={values(processCitiesStatsForMap(visits))}
locations={values(citiesForMap)}
/>
),
]}

View File

@@ -11,7 +11,7 @@ const propTypes = {
shortUrlVisits: shortUrlVisitsType.isRequired,
};
export function VisitsHeader({ shortUrlDetail, shortUrlVisits }) {
export default function VisitsHeader({ shortUrlDetail, shortUrlVisits }) {
const { shortUrl, loading } = shortUrlDetail;
const { visits } = shortUrlVisits;
const shortLink = shortUrl && shortUrl.shortUrl ? shortUrl.shortUrl : '';

View File

@@ -1,4 +1,4 @@
import { assoc, isNil, isEmpty, reduce } from 'ramda';
import { isNil, isEmpty, memoizeWith, prop, reduce } from 'ramda';
const osFromUserAgent = (userAgent) => {
const lowerUserAgent = userAgent.toLowerCase();
@@ -42,79 +42,70 @@ const extractDomain = (url) => {
return domain.split(':')[0];
};
export const processOsStats = (visits) =>
reduce(
(stats, { userAgent }) => {
const os = isNil(userAgent) ? 'Others' : osFromUserAgent(userAgent);
return assoc(os, (stats[os] || 0) + 1, stats);
},
{},
visits,
);
export const processBrowserStats = (visits) =>
reduce(
(stats, { userAgent }) => {
const browser = isNil(userAgent) ? 'Others' : browserFromUserAgent(userAgent);
return assoc(browser, (stats[browser] || 0) + 1, stats);
},
{},
visits,
);
export const processReferrersStats = (visits) =>
reduce(
(stats, visit) => {
const notHasDomain = isNil(visit.referer) || isEmpty(visit.referer);
const domain = notHasDomain ? 'Unknown' : extractDomain(visit.referer);
return assoc(domain, (stats[domain] || 0) + 1, stats);
},
{},
visits,
);
const visitLocationHasProperty = (visitLocation, propertyName) =>
!isNil(visitLocation)
&& !isNil(visitLocation[propertyName])
&& !isEmpty(visitLocation[propertyName]);
const buildLocationStatsProcessorByProperty = (propertyName) => (visits) =>
const updateOsStatsForVisit = (osStats, { userAgent }) => {
const os = isNil(userAgent) ? 'Others' : osFromUserAgent(userAgent);
osStats[os] = (osStats[os] || 0) + 1;
};
const updateBrowsersStatsForVisit = (browsersStats, { userAgent }) => {
const browser = isNil(userAgent) ? 'Others' : browserFromUserAgent(userAgent);
browsersStats[browser] = (browsersStats[browser] || 0) + 1;
};
const updateReferrersStatsForVisit = (referrersStats, { referer }) => {
const notHasDomain = isNil(referer) || isEmpty(referer);
const domain = notHasDomain ? 'Unknown' : extractDomain(referer);
referrersStats[domain] = (referrersStats[domain] || 0) + 1;
};
const updateLocationsStatsForVisit = (propertyName) => (stats, { visitLocation }) => {
const hasLocationProperty = visitLocationHasProperty(visitLocation, propertyName);
const value = hasLocationProperty ? visitLocation[propertyName] : 'Unknown';
stats[value] = (stats[value] || 0) + 1;
};
const updateCountriesStatsForVisit = updateLocationsStatsForVisit('countryName');
const updateCitiesStatsForVisit = updateLocationsStatsForVisit('cityName');
const updateCitiesForMapForVisit = (citiesForMapStats, { visitLocation }) => {
if (!visitLocationHasProperty(visitLocation, 'cityName')) {
return;
}
const { cityName, latitude, longitude } = visitLocation;
const currentCity = citiesForMapStats[cityName] || {
cityName,
count: 0,
latLong: [ parseFloat(latitude), parseFloat(longitude) ],
};
currentCity.count++;
citiesForMapStats[cityName] = currentCity;
};
export const processStatsFromVisits = memoizeWith(prop('id'), ({ visits }) =>
reduce(
(stats, { visitLocation }) => {
const hasLocationProperty = visitLocationHasProperty(visitLocation, propertyName);
const value = hasLocationProperty ? visitLocation[propertyName] : 'Unknown';
(stats, visit) => {
// We mutate the original object because it has a big side effect when large data sets are processed
updateOsStatsForVisit(stats.os, visit);
updateBrowsersStatsForVisit(stats.browsers, visit);
updateReferrersStatsForVisit(stats.referrers, visit);
updateCountriesStatsForVisit(stats.countries, visit);
updateCitiesStatsForVisit(stats.cities, visit);
updateCitiesForMapForVisit(stats.citiesForMap, visit);
return assoc(value, (stats[value] || 0) + 1, stats);
return stats;
},
{},
{ os: {}, browsers: {}, referrers: {}, countries: {}, cities: {}, citiesForMap: {} },
visits,
);
export const processCountriesStats = buildLocationStatsProcessorByProperty('countryName');
export const processCitiesStats = buildLocationStatsProcessorByProperty('cityName');
export const processCitiesStatsForMap = (visits) =>
reduce(
(stats, { visitLocation }) => {
if (!visitLocationHasProperty(visitLocation, 'cityName')) {
return stats;
}
const { cityName, latitude, longitude } = visitLocation;
const currentCity = stats[cityName] || {
cityName,
count: 0,
latLong: [ parseFloat(latitude), parseFloat(longitude) ],
};
currentCity.count++;
return assoc(cityName, currentCity, stats);
},
{},
visits,
);
));