Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Builder instead of String #18

Closed
wants to merge 19 commits into from

Conversation

parsonsmatt
Copy link
Contributor

@parsonsmatt parsonsmatt commented Dec 11, 2022

Fixes #16

This PR uses Data.ByteString.Builder instead of String for HTML fragments

Also uses difference lists to avoid the ++ penalty

There's no benchmark on the library, so I can't provide direct results. However, comparing this to my CPS WriterT branch on haddock, I'm seeing some nice improvements, particularly for such a local change:

With just the CPS WriterT

!!! ppHtml: finished in 574.35 milliseconds, allocated 1592.523 megabytes
*** Deleting temp dirs:
  26,145,652,864 bytes allocated in the heap
   4,170,952,440 bytes copied during GC
     278,830,600 bytes maximum residency (18 sample(s))
       5,591,544 bytes maximum slop
             621 MiB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0      6230 colls,     0 par    2.444s   2.448s     0.0004s    0.0040s
  Gen  1        18 colls,     0 par    1.423s   1.423s     0.0791s    0.2157s

  TASKS: 5 (1 bound, 4 peak workers (4 total), using -N1)

  SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.001s  (  0.000s elapsed)
  MUT     time   10.466s  ( 11.403s elapsed)
  GC      time    3.867s  (  3.871s elapsed)
  EXIT    time    0.001s  (  0.006s elapsed)
  Total   time   14.334s  ( 15.280s elapsed)

  Alloc rate    2,498,258,001 bytes per MUT second

  Productivity  73.0% of total user, 74.6% of total elapsed

Documentation created:
/home/matt/Projects/persistent/dist-newstyle/build/x86_64-linux/ghc-9.4.3/persistent-test-2.13.1.3/doc/html/persistent-test/index.html
cabal haddock --haddock-options "-v2 +RTS -s -RTS --optghc=\"-v2\""    16.09s user 1.53s system 98% cpu 17.834 total
  • HTML allocations: 1,592MB
  • HTML time: 574ms
  • Time: 17.834s
  • Mem in use: 621MB
  • Allocated: 26.145GB

Using a Builder variant of xhtml

!!! ppHtml: finished in 298.89 milliseconds, allocated 1578.839 megabytes

  26,199,628,152 bytes allocated in the heap
   3,842,361,072 bytes copied during GC
     204,699,600 bytes maximum residency (17 sample(s))
       5,185,792 bytes maximum slop
             547 MiB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0      6139 colls,     0 par    2.358s   2.362s     0.0004s    0.0039s
  Gen  1        17 colls,     0 par    1.168s   1.169s     0.0688s    0.1587s

  TASKS: 5 (1 bound, 4 peak workers (4 total), using -N1)

  SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.001s  (  0.000s elapsed)
  MUT     time   10.425s  ( 11.357s elapsed)
  GC      time    3.526s  (  3.531s elapsed)
  EXIT    time    0.001s  (  0.002s elapsed)
  Total   time   13.953s  ( 14.890s elapsed)

  Alloc rate    2,513,036,906 bytes per MUT second

  Productivity  74.7% of total user, 76.3% of total elapsed

Documentation created:
/home/matt/Projects/persistent/dist-newstyle/build/x86_64-linux/ghc-9.4.3/persistent-test-2.13.1.3/doc/html/persistent-test/index.html
cabal haddock --haddock-options "-v2 +RTS -s -RTS --optghc=\"-v2\""    14.38s user 0.89s system 100% cpu 15.236 total
  • HTML allocations: 1,578MB (-14MB) negligible
  • HTML time: 299ms (-275ms) (48% reduction)
  • Mem in use: 547MB (-74MB) (12% reduction)

Surprisingly, overall allocations are about the same. I'd have expected the allocations to be much less, since the overhead of a Builder is smaller than a String.

The reduced memory in use is stable across haddock runs. I'm not entirely sure why that's the case, given that allocations are basically unchanged.

@parsonsmatt parsonsmatt changed the title Mattp/builder Use Builder instead of String Dec 11, 2022
@cdornan
Copy link
Member

cdornan commented Dec 12, 2022

This is awesome @parsonsmatt — I am completely sold on this being the right way to go and am entirely concerned about not breaking anything.

What are the risks as you see it?

@parsonsmatt
Copy link
Contributor Author

Well, after doing some benchmarking and profiling, I'm unconvinced this is the right approach.

It is a noticeable improvement. But haddock's HTML rendering in persistent-test is dominated by link generation - anchor ! [href str]. It's doing a ton of fixChar, which ends up taking an inordinate amount of time. I'm also suspicious that it may be repeating this.

So I want to do a bit more benchmarking before I commit this in

@cdornan
Copy link
Member

cdornan commented Dec 13, 2022

By all means! My main concern remains avoiding breakage.

@parsonsmatt
Copy link
Contributor Author

Do you mean like API break? Or breaking change in behvaior?

There isn't a test suite. I suppose I could diff the result of a Haddock run with this path and before.

@cdornan
Copy link
Member

cdornan commented Dec 13, 2022

I mean breaking changes in behaviour and I am concerned because we have no test coverage.

So I really have to rely on good old fashioned engineering judgement — yours! If you think the risks are reasonable then I am good with that.

@cdornan
Copy link
Member

cdornan commented Dec 19, 2022

@parsonsmatt do you need anything from me? (All good on my side — just checking I am not blocking anything.)

@parsonsmatt
Copy link
Contributor Author

No, I just want to ensure this is actually a performance boon on Haddock. I haven't been able to verify that it speeds anything up or that it's correct yet.

@cdornan
Copy link
Member

cdornan commented Dec 20, 2022

all cool — just ping me if you need anything

where
close = if empty then " />" else ">"

shownAttrs = concat [' ':showPair attr | attr <- attrs ]
shownAttrs = foldMap (\attr -> charUtf8 ' ' <> showPair attr) attrs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my equivalence testing, the attributes are showing up in reverse order. Weird. The difference should be pretty small.

concat [ k x | x <- xs ] is equal to concat $ map k xs, which is concatMap k xs, and concatMap = foldMap when m ~ [b], as it is here, so the real diff is

-foldMap (\attr -> ' ' : showPair attr) attrs
+foldMap (\attr -> charUtf8 ' ' <> showPair attr) attrs

So the rendering isn't the issue.

Comment on lines -114 to -115
addAttrs (html@(HtmlTag { markupAttrs = attrs }) )
= html { markupAttrs = attrs ++ attr }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here we have attrs ++ attr, with attr being the new thing. So:

(anchor ! [href "asdf"]) ! [theclass "woop"]

should evaluate to:

(anchor ! [href "asdf"]) ! [theclass "asdf"]
(Html { attrs = [href "asdf"] }) ! [theclass "asdf"]
(Html { attrs = [href "asdf"] ++ [theclass "asdf"] })

case html of
HtmlTag { markupAttrs = attrs, .. } ->
HtmlTag
{ markupAttrs = attrs . (++ attr)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But here, we have:

(anchor ! [href "asdf"]) ! [theclass "qwer"]
==>
(HtmlTag { markupAttrs = id } ! [href "asdf"]) ! [theclass "qwer"]
==>
HtmlTag { markupAttrs = id . (++ [href "asdf"]) } ! [theclass "qwer"]
==>
HtmlTag { markupAttrs = id . (++ [href "asdf"]) . (++ [theclass "qwer"]) }

When we go to render, we apply [] to this. GHCI makes that:

λ> id . (++ ["asdf"]) . (++ ["qwer"]) $ []
["qwer","asdf"]

Which is - hm, a bit surprising!

id . (++ xs) . (++ ys) $ []
-- delete id
(++ xs) . (++ ys) $ []
-- replace `.` with `$`
(++ xs) $ (++ ys) []
-- inline operator section
(++ xs) $ [] ++ ys
-- inline operator
([] ++ ys) ++ xs

The dlist package defines fromList as UnsafeDList . (++) and append as UnsafeDList $ unsafeApplyDList xs . unsafeApplyDList ys.

fromList xs = UnsafeDList . (++) $ xs
fromList xs = UnsafeDList $ (++) xs
fromList xs = UnsafeDList (xs ++)

So, yeah, I converted attr incorrectly here.

@parsonsmatt
Copy link
Contributor Author

Verified that there's no diff for persistent generated documentation with this patch and without on haddock 🎉

Final performance improvement:

Comparing Haddock Head and the most recent, we get:

Haddock Head xhtml Builder Absolute Difference Relative Change
HTML allocations 1134 MB 1141 MB +7 MB 0.6% worse
HTML time: 380 ms 198 ms -182 ms 47.9% improvement
Total Memory: 554 MB 466 MB -88 MB 15.9% improvement
Total Allocated: 16.0 GB 16.0 GB 0 No change
Max residency: 238 MB 195 MB -43 MB 18.1% improvement
Total Time: 10.88 s 6.526s s -4.354 s 40% improvement

Pretty pleased with these results.

@parsonsmatt
Copy link
Contributor Author

@cdornan I believe this is ready for merge and release

@parsonsmatt
Copy link
Contributor Author

Unfortunately, this API is unpleasant with OverloadedStrings. Can see in the Haddock PR: https://github.com/haskell/haddock/pull/1546/files

Because many functions accept HTML a => a, OverloadedStrings causes a string literal to be unable to resolve without a type application or signature.

@parsonsmatt
Copy link
Contributor Author

OK, this is really frustrating!

I can't reproduce the performance findings. In fact, when I try now, the Builder variant is considerably worse, on all metrics.

Going to close this out for now until I can figure out what's going on.

@cdornan
Copy link
Member

cdornan commented Dec 23, 2022

@parsonsmatt i am so sorry! And sorry I could not be of more help and thanks for looking into this. I hope you have a good break.

@parsonsmatt
Copy link
Contributor Author

I'm thinking it may be an issue with GHC 9.4.2, actually? GHC 9.4.3 shows a big speedup. But the work codebase uses GHC 9.4.2, and I can't reproduce the performance benefits with 9.4.2.

@cdornan
Copy link
Member

cdornan commented Dec 23, 2022

That could explain it. Feel free to reopen once your confidence is restored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use a different type than String
2 participants