threadSelector.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import {useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import CompactSelect from 'sentry/components/compactSelect';
  4. import {IconList} from 'sentry/icons';
  5. import {tn} from 'sentry/locale';
  6. import space from 'sentry/styles/space';
  7. import {SelectValue} from 'sentry/types';
  8. import {defined} from 'sentry/utils';
  9. import {FlamegraphState} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext';
  10. import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  11. import {Profile} from 'sentry/utils/profiling/profile/profile';
  12. import {makeFormatter} from 'sentry/utils/profiling/units/units';
  13. interface ThreadSelectorProps {
  14. onThreadIdChange: (threadId: Profile['threadId']) => void;
  15. profileGroup: ProfileGroup;
  16. threadId: FlamegraphState['profiles']['threadId'];
  17. }
  18. function ThreadMenuSelector({
  19. threadId,
  20. onThreadIdChange,
  21. profileGroup,
  22. }: ThreadSelectorProps) {
  23. const options: SelectValue<number>[] = useMemo(() => {
  24. return [...profileGroup.profiles].sort(compareProfiles).map(profile => {
  25. return {
  26. label: profile.name
  27. ? `tid (${profile.threadId}): ${profile.name}`
  28. : `tid (${profile.threadId})`,
  29. value: profile.threadId,
  30. disabled: profile.samples.length === 0,
  31. details: (
  32. <ThreadLabelDetails
  33. duration={makeFormatter(profile.unit)(profile.duration)}
  34. // plus 1 because the last sample always has a weight of 0
  35. // and is not included in the raw weights
  36. samples={profile.rawWeights.length + 1}
  37. />
  38. ),
  39. };
  40. });
  41. }, [profileGroup]);
  42. const handleChange: (opt: SelectValue<number>) => void = useCallback(
  43. opt => {
  44. if (defined(opt)) {
  45. onThreadIdChange(opt.value);
  46. }
  47. },
  48. [onThreadIdChange]
  49. );
  50. return (
  51. <CompactSelect
  52. triggerProps={{
  53. icon: <IconList size="xs" />,
  54. size: 'xs',
  55. }}
  56. options={options}
  57. value={threadId}
  58. onChange={handleChange}
  59. isSearchable
  60. />
  61. );
  62. }
  63. interface ThreadLabelDetailsProps {
  64. duration: string;
  65. samples: number;
  66. }
  67. function ThreadLabelDetails(props: ThreadLabelDetailsProps) {
  68. return (
  69. <DetailsContainer>
  70. <div>{props.duration}</div>
  71. <div>{tn('%s sample', '%s samples', props.samples)}</div>
  72. </DetailsContainer>
  73. );
  74. }
  75. type ProfileLight = {
  76. duration: Profile['duration'];
  77. name: Profile['name'];
  78. threadId: Profile['threadId'];
  79. };
  80. function compareProfiles(a: ProfileLight, b: ProfileLight): number {
  81. if (!b.duration) {
  82. return -1;
  83. }
  84. if (!a.duration) {
  85. return 1;
  86. }
  87. if (a.name.startsWith('(tid') && b.name.startsWith('(tid')) {
  88. return -1;
  89. }
  90. if (a.name.startsWith('(tid')) {
  91. return -1;
  92. }
  93. if (b.name.startsWith('(tid')) {
  94. return -1;
  95. }
  96. if (a.name.includes('main')) {
  97. return -1;
  98. }
  99. if (b.name.includes('main')) {
  100. return 1;
  101. }
  102. return a.name > b.name ? -1 : 1;
  103. }
  104. const DetailsContainer = styled('div')`
  105. display: flex;
  106. flex-direction: row;
  107. justify-content: space-between;
  108. gap: ${space(1)};
  109. `;
  110. export {ThreadMenuSelector};