Нет описания

profile_test.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. // Copyright 2014 Google Inc. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package profile
  15. import (
  16. "bytes"
  17. "fmt"
  18. "io/ioutil"
  19. "path/filepath"
  20. "regexp"
  21. "strings"
  22. "sync"
  23. "testing"
  24. "github.com/google/pprof/internal/proftest"
  25. )
  26. func TestParse(t *testing.T) {
  27. const path = "testdata/"
  28. for _, source := range []string{
  29. "go.crc32.cpu",
  30. "go.godoc.thread",
  31. "gobench.cpu",
  32. "gobench.heap",
  33. "cppbench.cpu",
  34. "cppbench.heap",
  35. "cppbench.contention",
  36. "cppbench.growth",
  37. "cppbench.thread",
  38. "cppbench.thread.all",
  39. "cppbench.thread.none",
  40. "java.cpu",
  41. "java.heap",
  42. "java.contention",
  43. } {
  44. inbytes, err := ioutil.ReadFile(filepath.Join(path, source))
  45. if err != nil {
  46. t.Fatal(err)
  47. }
  48. p, err := Parse(bytes.NewBuffer(inbytes))
  49. if err != nil {
  50. t.Fatalf("%s: %s", source, err)
  51. }
  52. js := p.String()
  53. goldFilename := path + source + ".string"
  54. gold, err := ioutil.ReadFile(goldFilename)
  55. if err != nil {
  56. t.Fatalf("%s: %v", source, err)
  57. }
  58. if js != string(gold) {
  59. t.Errorf("diff %s %s", source, goldFilename)
  60. d, err := proftest.Diff(gold, []byte(js))
  61. if err != nil {
  62. t.Fatalf("%s: %v", source, err)
  63. }
  64. t.Error(source + "\n" + string(d) + "\n" + "new profile at:\n" + leaveTempfile([]byte(js)))
  65. }
  66. // Reencode and decode.
  67. bw := bytes.NewBuffer(nil)
  68. if err := p.Write(bw); err != nil {
  69. t.Fatalf("%s: %v", source, err)
  70. }
  71. if p, err = Parse(bw); err != nil {
  72. t.Fatalf("%s: %v", source, err)
  73. }
  74. js2 := p.String()
  75. if js2 != string(gold) {
  76. d, err := proftest.Diff(gold, []byte(js2))
  77. if err != nil {
  78. t.Fatalf("%s: %v", source, err)
  79. }
  80. t.Error(source + "\n" + string(d) + "\n" + "gold:\n" + goldFilename +
  81. "\nnew profile at:\n" + leaveTempfile([]byte(js)))
  82. }
  83. }
  84. }
  85. func TestParseError(t *testing.T) {
  86. testcases := []string{
  87. "",
  88. "garbage text",
  89. "\x1f\x8b", // truncated gzip header
  90. "\x1f\x8b\x08\x08\xbe\xe9\x20\x58\x00\x03\x65\x6d\x70\x74\x79\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", // empty gzipped file
  91. }
  92. for i, input := range testcases {
  93. _, err := Parse(strings.NewReader(input))
  94. if err == nil {
  95. t.Errorf("got nil, want error for input #%d", i)
  96. }
  97. }
  98. }
  99. // leaveTempfile leaves |b| in a temporary file on disk and returns the
  100. // temp filename. This is useful to recover a profile when the test
  101. // fails.
  102. func leaveTempfile(b []byte) string {
  103. f1, err := ioutil.TempFile("", "profile_test")
  104. if err != nil {
  105. panic(err)
  106. }
  107. if _, err := f1.Write(b); err != nil {
  108. panic(err)
  109. }
  110. return f1.Name()
  111. }
  112. const mainBinary = "/bin/main"
  113. var cpuM = []*Mapping{
  114. {
  115. ID: 1,
  116. Start: 0x10000,
  117. Limit: 0x40000,
  118. File: mainBinary,
  119. HasFunctions: true,
  120. HasFilenames: true,
  121. HasLineNumbers: true,
  122. HasInlineFrames: true,
  123. },
  124. {
  125. ID: 2,
  126. Start: 0x1000,
  127. Limit: 0x4000,
  128. File: "/lib/lib.so",
  129. HasFunctions: true,
  130. HasFilenames: true,
  131. HasLineNumbers: true,
  132. HasInlineFrames: true,
  133. },
  134. {
  135. ID: 3,
  136. Start: 0x4000,
  137. Limit: 0x5000,
  138. File: "/lib/lib2_c.so.6",
  139. HasFunctions: true,
  140. HasFilenames: true,
  141. HasLineNumbers: true,
  142. HasInlineFrames: true,
  143. },
  144. {
  145. ID: 4,
  146. Start: 0x5000,
  147. Limit: 0x9000,
  148. File: "/lib/lib.so_6 (deleted)",
  149. HasFunctions: true,
  150. HasFilenames: true,
  151. HasLineNumbers: true,
  152. HasInlineFrames: true,
  153. },
  154. }
  155. var cpuF = []*Function{
  156. {ID: 1, Name: "main", SystemName: "main", Filename: "main.c"},
  157. {ID: 2, Name: "foo", SystemName: "foo", Filename: "foo.c"},
  158. {ID: 3, Name: "foo_caller", SystemName: "foo_caller", Filename: "foo.c"},
  159. }
  160. var cpuL = []*Location{
  161. {
  162. ID: 1000,
  163. Mapping: cpuM[1],
  164. Address: 0x1000,
  165. Line: []Line{
  166. {Function: cpuF[0], Line: 1},
  167. },
  168. },
  169. {
  170. ID: 2000,
  171. Mapping: cpuM[0],
  172. Address: 0x2000,
  173. Line: []Line{
  174. {Function: cpuF[1], Line: 2},
  175. {Function: cpuF[2], Line: 1},
  176. },
  177. },
  178. {
  179. ID: 3000,
  180. Mapping: cpuM[0],
  181. Address: 0x3000,
  182. Line: []Line{
  183. {Function: cpuF[1], Line: 2},
  184. {Function: cpuF[2], Line: 1},
  185. },
  186. },
  187. {
  188. ID: 3001,
  189. Mapping: cpuM[0],
  190. Address: 0x3001,
  191. Line: []Line{
  192. {Function: cpuF[2], Line: 2},
  193. },
  194. },
  195. {
  196. ID: 3002,
  197. Mapping: cpuM[0],
  198. Address: 0x3002,
  199. Line: []Line{
  200. {Function: cpuF[2], Line: 3},
  201. },
  202. },
  203. }
  204. var testProfile1 = &Profile{
  205. PeriodType: &ValueType{Type: "cpu", Unit: "milliseconds"},
  206. Period: 1,
  207. DurationNanos: 10e9,
  208. SampleType: []*ValueType{
  209. {Type: "samples", Unit: "count"},
  210. {Type: "cpu", Unit: "milliseconds"},
  211. },
  212. Sample: []*Sample{
  213. {
  214. Location: []*Location{cpuL[0]},
  215. Value: []int64{1000, 1000},
  216. Label: map[string][]string{
  217. "key1": {"tag1"},
  218. "key2": {"tag1"},
  219. },
  220. },
  221. {
  222. Location: []*Location{cpuL[1], cpuL[0]},
  223. Value: []int64{100, 100},
  224. Label: map[string][]string{
  225. "key1": {"tag2"},
  226. "key3": {"tag2"},
  227. },
  228. },
  229. {
  230. Location: []*Location{cpuL[2], cpuL[0]},
  231. Value: []int64{10, 10},
  232. Label: map[string][]string{
  233. "key1": {"tag3"},
  234. "key2": {"tag2"},
  235. },
  236. },
  237. {
  238. Location: []*Location{cpuL[3], cpuL[0]},
  239. Value: []int64{10000, 10000},
  240. Label: map[string][]string{
  241. "key1": {"tag4"},
  242. "key2": {"tag1"},
  243. },
  244. },
  245. {
  246. Location: []*Location{cpuL[4], cpuL[0]},
  247. Value: []int64{1, 1},
  248. Label: map[string][]string{
  249. "key1": {"tag4"},
  250. "key2": {"tag1"},
  251. },
  252. },
  253. },
  254. Location: cpuL,
  255. Function: cpuF,
  256. Mapping: cpuM,
  257. }
  258. var testProfile2 = &Profile{
  259. PeriodType: &ValueType{Type: "cpu", Unit: "milliseconds"},
  260. Period: 1,
  261. DurationNanos: 10e9,
  262. SampleType: []*ValueType{
  263. {Type: "samples", Unit: "count"},
  264. {Type: "cpu", Unit: "milliseconds"},
  265. },
  266. Sample: []*Sample{
  267. {
  268. Location: []*Location{cpuL[0]},
  269. Value: []int64{70, 1000},
  270. Label: map[string][]string{
  271. "key1": {"tag1"},
  272. "key2": {"tag1"},
  273. },
  274. },
  275. {
  276. Location: []*Location{cpuL[1], cpuL[0]},
  277. Value: []int64{60, 100},
  278. Label: map[string][]string{
  279. "key1": {"tag2"},
  280. "key3": {"tag2"},
  281. },
  282. },
  283. {
  284. Location: []*Location{cpuL[2], cpuL[0]},
  285. Value: []int64{50, 10},
  286. Label: map[string][]string{
  287. "key1": {"tag3"},
  288. "key2": {"tag2"},
  289. },
  290. },
  291. {
  292. Location: []*Location{cpuL[3], cpuL[0]},
  293. Value: []int64{40, 10000},
  294. Label: map[string][]string{
  295. "key1": {"tag4"},
  296. "key2": {"tag1"},
  297. },
  298. },
  299. {
  300. Location: []*Location{cpuL[4], cpuL[0]},
  301. Value: []int64{1, 1},
  302. Label: map[string][]string{
  303. "key1": {"tag4"},
  304. "key2": {"tag1"},
  305. },
  306. },
  307. },
  308. Location: cpuL,
  309. Function: cpuF,
  310. Mapping: cpuM,
  311. }
  312. var testProfile3 = &Profile{
  313. PeriodType: &ValueType{Type: "cpu", Unit: "milliseconds"},
  314. Period: 1,
  315. DurationNanos: 10e9,
  316. SampleType: []*ValueType{
  317. {Type: "samples", Unit: "count"},
  318. },
  319. Sample: []*Sample{
  320. {
  321. Location: []*Location{cpuL[0]},
  322. Value: []int64{1000},
  323. Label: map[string][]string{
  324. "key1": {"tag1"},
  325. "key2": {"tag1"},
  326. },
  327. },
  328. },
  329. Location: cpuL,
  330. Function: cpuF,
  331. Mapping: cpuM,
  332. }
  333. var aggTests = map[string]aggTest{
  334. "precise": {true, true, true, true, 5},
  335. "fileline": {false, true, true, true, 4},
  336. "inline_function": {false, true, false, true, 3},
  337. "function": {false, true, false, false, 2},
  338. }
  339. type aggTest struct {
  340. precise, function, fileline, inlineFrame bool
  341. rows int
  342. }
  343. const totalSamples = int64(11111)
  344. func TestAggregation(t *testing.T) {
  345. prof := testProfile1.Copy()
  346. for _, resolution := range []string{"precise", "fileline", "inline_function", "function"} {
  347. a := aggTests[resolution]
  348. if !a.precise {
  349. if err := prof.Aggregate(a.inlineFrame, a.function, a.fileline, a.fileline, false); err != nil {
  350. t.Error("aggregating to " + resolution + ":" + err.Error())
  351. }
  352. }
  353. if err := checkAggregation(prof, &a); err != nil {
  354. t.Error("failed aggregation to " + resolution + ": " + err.Error())
  355. }
  356. }
  357. }
  358. // checkAggregation verifies that the profile remained consistent
  359. // with its aggregation.
  360. func checkAggregation(prof *Profile, a *aggTest) error {
  361. // Check that the total number of samples for the rows was preserved.
  362. total := int64(0)
  363. samples := make(map[string]bool)
  364. for _, sample := range prof.Sample {
  365. tb := locationHash(sample)
  366. samples[tb] = true
  367. total += sample.Value[0]
  368. }
  369. if total != totalSamples {
  370. return fmt.Errorf("sample total %d, want %d", total, totalSamples)
  371. }
  372. // Check the number of unique sample locations
  373. if a.rows != len(samples) {
  374. return fmt.Errorf("number of samples %d, want %d", len(samples), a.rows)
  375. }
  376. // Check that all mappings have the right detail flags.
  377. for _, m := range prof.Mapping {
  378. if m.HasFunctions != a.function {
  379. return fmt.Errorf("unexpected mapping.HasFunctions %v, want %v", m.HasFunctions, a.function)
  380. }
  381. if m.HasFilenames != a.fileline {
  382. return fmt.Errorf("unexpected mapping.HasFilenames %v, want %v", m.HasFilenames, a.fileline)
  383. }
  384. if m.HasLineNumbers != a.fileline {
  385. return fmt.Errorf("unexpected mapping.HasLineNumbers %v, want %v", m.HasLineNumbers, a.fileline)
  386. }
  387. if m.HasInlineFrames != a.inlineFrame {
  388. return fmt.Errorf("unexpected mapping.HasInlineFrames %v, want %v", m.HasInlineFrames, a.inlineFrame)
  389. }
  390. }
  391. // Check that aggregation has removed finer resolution data.
  392. for _, l := range prof.Location {
  393. if !a.inlineFrame && len(l.Line) > 1 {
  394. return fmt.Errorf("found %d lines on location %d, want 1", len(l.Line), l.ID)
  395. }
  396. for _, ln := range l.Line {
  397. if !a.fileline && (ln.Function.Filename != "" || ln.Line != 0) {
  398. return fmt.Errorf("found line %s:%d on location %d, want :0",
  399. ln.Function.Filename, ln.Line, l.ID)
  400. }
  401. if !a.function && (ln.Function.Name != "") {
  402. return fmt.Errorf(`found file %s location %d, want ""`,
  403. ln.Function.Name, l.ID)
  404. }
  405. }
  406. }
  407. return nil
  408. }
  409. // Test merge leaves the main binary in place.
  410. func TestMergeMain(t *testing.T) {
  411. prof := testProfile1.Copy()
  412. p1, err := Merge([]*Profile{prof})
  413. if err != nil {
  414. t.Fatalf("merge error: %v", err)
  415. }
  416. if cpuM[0].File != p1.Mapping[0].File {
  417. t.Errorf("want Mapping[0]=%s got %s", cpuM[0].File, p1.Mapping[0].File)
  418. }
  419. }
  420. func TestMerge(t *testing.T) {
  421. // Aggregate a profile with itself and once again with a factor of
  422. // -2. Should end up with an empty profile (all samples for a
  423. // location should add up to 0).
  424. prof := testProfile1.Copy()
  425. p1, err := Merge([]*Profile{prof, prof})
  426. if err != nil {
  427. t.Errorf("merge error: %v", err)
  428. }
  429. prof.Scale(-2)
  430. prof, err = Merge([]*Profile{p1, prof})
  431. if err != nil {
  432. t.Errorf("merge error: %v", err)
  433. }
  434. // Use aggregation to merge locations at function granularity.
  435. if err := prof.Aggregate(false, true, false, false, false); err != nil {
  436. t.Errorf("aggregating after merge: %v", err)
  437. }
  438. samples := make(map[string]int64)
  439. for _, s := range prof.Sample {
  440. tb := locationHash(s)
  441. samples[tb] = samples[tb] + s.Value[0]
  442. }
  443. for s, v := range samples {
  444. if v != 0 {
  445. t.Errorf("nonzero value for sample %s: %d", s, v)
  446. }
  447. }
  448. }
  449. func TestMergeAll(t *testing.T) {
  450. // Aggregate 10 copies of the profile.
  451. profs := make([]*Profile, 10)
  452. for i := 0; i < 10; i++ {
  453. profs[i] = testProfile1.Copy()
  454. }
  455. prof, err := Merge(profs)
  456. if err != nil {
  457. t.Errorf("merge error: %v", err)
  458. }
  459. samples := make(map[string]int64)
  460. for _, s := range prof.Sample {
  461. tb := locationHash(s)
  462. samples[tb] = samples[tb] + s.Value[0]
  463. }
  464. for _, s := range testProfile1.Sample {
  465. tb := locationHash(s)
  466. if samples[tb] != s.Value[0]*10 {
  467. t.Errorf("merge got wrong value at %s : %d instead of %d", tb, samples[tb], s.Value[0]*10)
  468. }
  469. }
  470. }
  471. func TestNormalizeBySameProfile(t *testing.T) {
  472. pb := testProfile1.Copy()
  473. p := testProfile1.Copy()
  474. if err := p.Normalize(pb); err != nil {
  475. t.Fatal(err)
  476. }
  477. for i, s := range p.Sample {
  478. for j, v := range s.Value {
  479. expectedSampleValue := testProfile1.Sample[i].Value[j]
  480. if v != expectedSampleValue {
  481. t.Errorf("For sample %d, value %d want %d got %d", i, j, expectedSampleValue, v)
  482. }
  483. }
  484. }
  485. }
  486. func TestNormalizeByDifferentProfile(t *testing.T) {
  487. p := testProfile1.Copy()
  488. pb := testProfile2.Copy()
  489. if err := p.Normalize(pb); err != nil {
  490. t.Fatal(err)
  491. }
  492. expectedSampleValues := [][]int64{
  493. {19, 1000},
  494. {1, 100},
  495. {0, 10},
  496. {198, 10000},
  497. {0, 1},
  498. }
  499. for i, s := range p.Sample {
  500. for j, v := range s.Value {
  501. if v != expectedSampleValues[i][j] {
  502. t.Errorf("For sample %d, value %d want %d got %d", i, j, expectedSampleValues[i][j], v)
  503. }
  504. }
  505. }
  506. }
  507. func TestNormalizeByMultipleOfSameProfile(t *testing.T) {
  508. pb := testProfile1.Copy()
  509. for i, s := range pb.Sample {
  510. for j, v := range s.Value {
  511. pb.Sample[i].Value[j] = 10 * v
  512. }
  513. }
  514. p := testProfile1.Copy()
  515. err := p.Normalize(pb)
  516. if err != nil {
  517. t.Fatal(err)
  518. }
  519. for i, s := range p.Sample {
  520. for j, v := range s.Value {
  521. expectedSampleValue := 10 * testProfile1.Sample[i].Value[j]
  522. if v != expectedSampleValue {
  523. t.Errorf("For sample %d, value %d, want %d got %d", i, j, expectedSampleValue, v)
  524. }
  525. }
  526. }
  527. }
  528. func TestNormalizeIncompatibleProfiles(t *testing.T) {
  529. p := testProfile1.Copy()
  530. pb := testProfile3.Copy()
  531. if err := p.Normalize(pb); err == nil {
  532. t.Errorf("Expected an error")
  533. }
  534. }
  535. func TestFilter(t *testing.T) {
  536. // Perform several forms of filtering on the test profile.
  537. type filterTestcase struct {
  538. focus, ignore, hide, show *regexp.Regexp
  539. fm, im, hm, hnm bool
  540. }
  541. for tx, tc := range []filterTestcase{
  542. {
  543. fm: true, // nil focus matches every sample
  544. },
  545. {
  546. focus: regexp.MustCompile("notfound"),
  547. },
  548. {
  549. ignore: regexp.MustCompile("foo.c"),
  550. fm: true,
  551. im: true,
  552. },
  553. {
  554. hide: regexp.MustCompile("lib.so"),
  555. fm: true,
  556. hm: true,
  557. },
  558. {
  559. show: regexp.MustCompile("foo.c"),
  560. fm: true,
  561. hnm: true,
  562. },
  563. {
  564. show: regexp.MustCompile("notfound"),
  565. fm: true,
  566. },
  567. } {
  568. prof := *testProfile1.Copy()
  569. gf, gi, gh, gnh := prof.FilterSamplesByName(tc.focus, tc.ignore, tc.hide, tc.show)
  570. if gf != tc.fm {
  571. t.Errorf("Filter #%d, got fm=%v, want %v", tx, gf, tc.fm)
  572. }
  573. if gi != tc.im {
  574. t.Errorf("Filter #%d, got im=%v, want %v", tx, gi, tc.im)
  575. }
  576. if gh != tc.hm {
  577. t.Errorf("Filter #%d, got hm=%v, want %v", tx, gh, tc.hm)
  578. }
  579. if gnh != tc.hnm {
  580. t.Errorf("Filter #%d, got hnm=%v, want %v", tx, gnh, tc.hnm)
  581. }
  582. }
  583. }
  584. func TestTagFilter(t *testing.T) {
  585. // Perform several forms of tag filtering on the test profile.
  586. type filterTestcase struct {
  587. include, exclude *regexp.Regexp
  588. im, em bool
  589. count int
  590. }
  591. countTags := func(p *Profile) map[string]bool {
  592. tags := make(map[string]bool)
  593. for _, s := range p.Sample {
  594. for l := range s.Label {
  595. tags[l] = true
  596. }
  597. for l := range s.NumLabel {
  598. tags[l] = true
  599. }
  600. }
  601. return tags
  602. }
  603. for tx, tc := range []filterTestcase{
  604. {nil, nil, true, false, 3},
  605. {regexp.MustCompile("notfound"), nil, false, false, 0},
  606. {regexp.MustCompile("key1"), nil, true, false, 1},
  607. {nil, regexp.MustCompile("key[12]"), true, true, 1},
  608. } {
  609. prof := testProfile1.Copy()
  610. gim, gem := prof.FilterTagsByName(tc.include, tc.exclude)
  611. if gim != tc.im {
  612. t.Errorf("Filter #%d, got include match=%v, want %v", tx, gim, tc.im)
  613. }
  614. if gem != tc.em {
  615. t.Errorf("Filter #%d, got exclude match=%v, want %v", tx, gem, tc.em)
  616. }
  617. if tags := countTags(prof); len(tags) != tc.count {
  618. t.Errorf("Filter #%d, got %d tags[%v], want %d", tx, len(tags), tags, tc.count)
  619. }
  620. }
  621. }
  622. // locationHash constructs a string to use as a hashkey for a sample, based on its locations
  623. func locationHash(s *Sample) string {
  624. var tb string
  625. for _, l := range s.Location {
  626. for _, ln := range l.Line {
  627. tb = tb + fmt.Sprintf("%s:%d@%d ", ln.Function.Name, ln.Line, l.Address)
  628. }
  629. }
  630. return tb
  631. }
  632. func TestSetMain(t *testing.T) {
  633. testProfile1.massageMappings()
  634. if testProfile1.Mapping[0].File != mainBinary {
  635. t.Errorf("got %s for main", testProfile1.Mapping[0].File)
  636. }
  637. }
  638. // parallel runs n copies of fn in parallel.
  639. func parallel(n int, fn func()) {
  640. var wg sync.WaitGroup
  641. wg.Add(n)
  642. for i := 0; i < n; i++ {
  643. go func() {
  644. fn()
  645. wg.Done()
  646. }()
  647. }
  648. wg.Wait()
  649. }
  650. func TestThreadSafety(t *testing.T) {
  651. src := testProfile1.Copy()
  652. parallel(4, func() { src.Copy() })
  653. parallel(4, func() {
  654. var b bytes.Buffer
  655. src.WriteUncompressed(&b)
  656. })
  657. parallel(4, func() {
  658. var b bytes.Buffer
  659. src.Write(&b)
  660. })
  661. }