threadSelector.tsx 3.0 KB

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