bundlediff.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import Card from 'sentry/components/card';
  4. import * as Layout from 'sentry/components/layouts/thirds';
  5. import ExternalLink from 'sentry/components/links/externalLink';
  6. import PanelTable from 'sentry/components/panels/panelTable';
  7. import {SegmentedControl} from 'sentry/components/segmentedControl';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {formatBytesBase10} from 'sentry/utils';
  11. import useRouter from 'sentry/utils/useRouter';
  12. import {bundleStats as stats} from 'sentry/views/bundleAnalyzer';
  13. const beforeStats = require('./stats.json');
  14. import {CompactSelect} from 'sentry/components/compactSelect';
  15. import {formatPercentage} from 'sentry/utils/formatters';
  16. import {CardSection} from 'sentry/views/performance/transactionSummary/transactionVitals/styles';
  17. enum BundleType {
  18. ASSETS = 'assets',
  19. IN_APP = 'in_app',
  20. PACKAGES = 'packages',
  21. OTHER = 'other',
  22. }
  23. enum BundleState {
  24. REMOVED = 'removed',
  25. INCREASED = 'increased',
  26. NO_CHANGE = 'no_change',
  27. DECREASED = 'decreased',
  28. ADDED = 'added',
  29. }
  30. const BUNDLE_TYPES = [
  31. {
  32. key: BundleType.ASSETS,
  33. label: t('Assets'),
  34. },
  35. {
  36. key: BundleType.IN_APP,
  37. label: t('In App'),
  38. },
  39. {
  40. key: BundleType.PACKAGES,
  41. label: t('Packages'),
  42. },
  43. // {
  44. // key: BundleType.OTHER,
  45. // label: t('Other'),
  46. // },
  47. ];
  48. const chunks = stats.chunks.map(chunk => ({...chunk, name: chunk.id}));
  49. const assets = stats.assets.sort((a, b) => b.size - a.size);
  50. const modules = stats.modules;
  51. const inAppModules = modules
  52. .filter(
  53. m =>
  54. m.moduleType.startsWith('javascript') &&
  55. m.name.startsWith('./app') &&
  56. !m.name.endsWith('namespace object')
  57. )
  58. .sort((a, b) => b.size - a.size);
  59. const packageModules = modules
  60. .filter(m => m.name.startsWith('../node_modules'))
  61. .sort((a, b) => b.size - a.size);
  62. const beforeChunks = beforeStats.chunks.map(chunk => ({...chunk, name: chunk.id}));
  63. // const beforeAssets = beforeStats.assets.sort((a, b) => b.size - a.size);
  64. const beforeModules = beforeStats.modules;
  65. const beforeInAppModules = beforeModules
  66. .filter(
  67. m =>
  68. m.moduleType.startsWith('javascript') &&
  69. m.name.startsWith('./app') &&
  70. !m.name.endsWith('namespace object')
  71. )
  72. .sort((a, b) => b.size - a.size);
  73. const beforePackageModules = beforeModules
  74. .filter(m => m.name.startsWith('../node_modules'))
  75. .sort((a, b) => b.size - a.size);
  76. export default function BundleDiff() {
  77. const router = useRouter();
  78. const bundleType = router.location.query.bundleType ?? BundleType.ASSETS;
  79. function handleBundleAnalysisSelection(type: BundleType) {
  80. router.replace({
  81. ...router.location,
  82. query: {
  83. ...router.location.query,
  84. bundleType: type,
  85. },
  86. });
  87. }
  88. function getPanelItems() {
  89. switch (bundleType) {
  90. case BundleType.ASSETS: {
  91. return assets.map(asset => (
  92. <Fragment key={`asset-${asset.name}`}>
  93. <div>{asset.name}</div>
  94. <div>{formatBytesBase10(asset.size)}</div>
  95. <div>-</div>
  96. <div>-</div>
  97. </Fragment>
  98. ));
  99. }
  100. case BundleType.IN_APP: {
  101. return diff(beforeInAppModules, inAppModules)
  102. .filter(m => m.state !== BundleState.NO_CHANGE)
  103. .sort((a, b) => Math.abs(b.diff) - Math.abs(a.diff))
  104. .map(module => (
  105. <Fragment key={`module-${module.name}-${module.size}`}>
  106. <ExternalLink
  107. href={`https://github.com/getsentry/sentry/blob/master/static/${module.name.replace(
  108. './',
  109. ''
  110. )}`}
  111. >
  112. {module.name}
  113. </ExternalLink>
  114. <div>{formatBytesBase10(module.size)}</div>
  115. <div>{module.state}</div>
  116. <div>{formatPercentage(module.diff)}</div>
  117. </Fragment>
  118. ));
  119. }
  120. case BundleType.PACKAGES: {
  121. return diff(beforePackageModules, packageModules)
  122. .filter(m => m.state !== BundleState.NO_CHANGE)
  123. .sort((a, b) => Math.abs(b.diff) - Math.abs(a.diff))
  124. .map(module => {
  125. const npmPackageName = getNpmPackageFromNodeModules(module.name);
  126. return (
  127. <Fragment key={`module-${module.name}-${module.size}`}>
  128. <ExternalLink href={`https://www.npmjs.com/package/${npmPackageName}`}>
  129. {module.name.replace('../node_modules/', '')}
  130. </ExternalLink>
  131. <div>{formatBytesBase10(module.size)}</div>
  132. <div>{module.state}</div>
  133. <div>{formatPercentage(module.diff)}</div>
  134. </Fragment>
  135. );
  136. });
  137. }
  138. default: {
  139. throw new Error('Invalid bundle type');
  140. }
  141. }
  142. }
  143. const beforeEntryPoints = beforeChunks.filter(c => c.entry);
  144. const entrypoints = chunks.filter(c => c.entry);
  145. // console.log(beforeEntryPoints);
  146. // console.log(entrypoints);
  147. return (
  148. <Layout.Body>
  149. <Layout.Main fullWidth>
  150. <CompactSelect
  151. triggerLabel="aecd977f813ca1ac63a4123d5404230084938fa7"
  152. value="aecd977f813ca1ac63a4123d5404230084938fa7"
  153. triggerProps={{prefix: t('Compare')}}
  154. options={[
  155. {
  156. value: 'aecd977f813ca1ac63a4123d5404230084938fa7',
  157. textValue: 'aecd977f813ca1ac63a4123d5404230084938fa7',
  158. label: 'aecd977f813ca1ac63a4123d5404230084938fa7',
  159. },
  160. ]}
  161. onChange={() => undefined}
  162. />
  163. <CardWrapper>
  164. {diff(beforeEntryPoints, entrypoints).map(entrypoint => (
  165. <StyledCard key={entrypoint.name}>
  166. <CardSection>
  167. <div>{entrypoint.name}</div>
  168. <StatNumber>{formatBytesBase10(entrypoint.size)}</StatNumber>
  169. <div>({formatPercentage(entrypoint.diff)})</div>
  170. </CardSection>
  171. </StyledCard>
  172. ))}
  173. </CardWrapper>
  174. <SegmentedControlWrapper key="segmented-control">
  175. <SegmentedControl
  176. aria-label={t('Bundle Analysis Type')}
  177. size="sm"
  178. value={bundleType}
  179. onChange={key => handleBundleAnalysisSelection(key as BundleType)}
  180. >
  181. {BUNDLE_TYPES.map(({key, label}) => (
  182. <SegmentedControl.Item key={key} textValue={label}>
  183. {label}
  184. </SegmentedControl.Item>
  185. ))}
  186. </SegmentedControl>
  187. </SegmentedControlWrapper>
  188. <PanelTable headers={[t('Name'), t('Size'), t('Change'), t('Diff')]}>
  189. {getPanelItems()}
  190. </PanelTable>
  191. </Layout.Main>
  192. </Layout.Body>
  193. );
  194. }
  195. function getNpmPackageFromNodeModules(name: string): string {
  196. const path = name.replace('../node_modules/', '');
  197. const pathComponents = path.split('/');
  198. for (let i = pathComponents.length - 1; i >= 0; i--) {
  199. if (pathComponents[i] === 'node_modules') {
  200. const maybePackageName = pathComponents[i + 1];
  201. if (maybePackageName.startsWith('@')) {
  202. return maybePackageName + pathComponents[i + 2];
  203. }
  204. return maybePackageName;
  205. }
  206. }
  207. if (pathComponents[0].startsWith('@')) {
  208. return pathComponents[0] + pathComponents[1];
  209. }
  210. return pathComponents[0];
  211. }
  212. type Bundle = {
  213. name: string;
  214. size: number;
  215. };
  216. type DiffedBundle = {
  217. diff: number;
  218. name: string;
  219. size: number;
  220. state: BundleState;
  221. };
  222. function diff(before: Bundle[], after: Bundle[]): DiffedBundle[] {
  223. const diffed = new Map<string, DiffedBundle>();
  224. before.forEach(b => {
  225. diffed.set(b.name, {
  226. diff: 0,
  227. name: b.name,
  228. size: b.size,
  229. state: BundleState.REMOVED,
  230. });
  231. });
  232. after.forEach(a => {
  233. const beforeEntry = diffed.get(a.name);
  234. if (beforeEntry) {
  235. diffed.set(a.name, {
  236. diff: (a.size - beforeEntry.size) / beforeEntry.size,
  237. name: a.name,
  238. size: a.size,
  239. state:
  240. a.size === beforeEntry.size
  241. ? BundleState.NO_CHANGE
  242. : a.size > beforeEntry.size
  243. ? BundleState.INCREASED
  244. : BundleState.DECREASED,
  245. });
  246. } else {
  247. diffed.set(a.name, {
  248. diff: 0,
  249. name: a.name,
  250. size: a.size,
  251. state: BundleState.ADDED,
  252. });
  253. }
  254. });
  255. return Array.from(diffed.values());
  256. }
  257. const StyledCard = styled(Card)`
  258. margin-right: ${space(1)};
  259. `;
  260. const SegmentedControlWrapper = styled('div')`
  261. padding-bottom: ${space(2)};
  262. `;
  263. export const StatNumber = styled('div')`
  264. font-size: 32px;
  265. `;
  266. export const CardWrapper = styled('div')`
  267. display: flex;
  268. margin-top: ${space(2)};
  269. `;