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

mpv -vo sixel is far superior to ncplayer -bpixel #1391

Closed
dankamongmen opened this issue Mar 8, 2021 · 63 comments · Fixed by #1418
Closed

mpv -vo sixel is far superior to ncplayer -bpixel #1391

dankamongmen opened this issue Mar 8, 2021 · 63 comments · Fixed by #1418
Assignees
Labels
bitmaps bitmapped graphics (sixel, kitty, mmap) bug Something isn't working perf sweet sweet perf
Milestone

Comments

@dankamongmen
Copy link
Owner

@dnkl pointed this out in #1380, but it's worth its own issue. Get two xterms up, along with a recent mpv build featuring libsixel support. Run it alongside ncplayer -bpixel. mpv's superiority is obvious (we beat it with cell output, though). Get to this level of output, in both speed and quality.

@dankamongmen dankamongmen added bug Something isn't working perf sweet sweet perf labels Mar 8, 2021
@dankamongmen dankamongmen added this to the 2.3.0 milestone Mar 8, 2021
@dankamongmen dankamongmen self-assigned this Mar 8, 2021
@dankamongmen
Copy link
Owner Author

Ahhh, I think the performance advantage is coming from damage tracking internal to the media. That makes total sense. Hrmmmmm. That will not be trivial to fit into our model, though...I think we needed something like this for #1360, right?

Alright, so if we brought the previous bitmap into sixel_blit, we'd leave out any pixels which matched in their sixelspace reduction. Makes sense.

but that can't be all. We'd expect our first frames to look roughly equivalent, but they are not. Hrmm, let's limit comparison to the first frame...

@dankamongmen
Copy link
Owner Author

ahhh yes, i noticed this the other day -- we emit a band for every color, even if the color isn't yet used. of course. let's strip those and see how things improve.

@dankamongmen
Copy link
Owner Author

@dnkl i've cut out empty bands, and it cut the total output per frame by about 40%. we still emit longer chunks than mpv, because mpv is using more colors, so it has shorter chunks of the same color. our total line lengths ought be about the same -- i'm looking into this now. either way, a big win in terms of output size, thanks for the heads-up!

@dankamongmen
Copy link
Owner Author

Ahhh, I think the performance advantage is coming from damage tracking internal to the media. That makes total sense. Hrmmmmm. That will not be trivial to fit into our model, though...I think we needed something like this for #1360, right?
Alright, so if we brought the previous bitmap into sixel_blit, we'd leave out any pixels which matched in their sixelspace reduction. Makes sense.

erp...damage tracking in this way wouldn't play nicely with moving the image :(

@dankamongmen
Copy link
Owner Author

for our first band on the first frame of fm6.wmv, i get

#32!362?A!27?A!55?B@!33?_?K[Kkk!15{w!14o!12?F!9?@!5B@!18?@BB@!3B!9F!3B@BB@!4?B!5~nNnF!17B!12F!8B!4?FN!4^!136~B!11nf!13?u!46~!10^!252~$

followed by

#33}!461?G[WG!8WG[!7^RB!3R!15BF!14N^!11NWF!3?BB!3FE!5CE!13F!5BACCE!3C?!8G!3CECCE@??F[!5?!3OG!17C!3?!8G?!8C!4F!142?{!11OW!13~H$

are these the same length?

@dankamongmen
Copy link
Owner Author

ok i don't understand what mpv is doing exactly. here's some sample output:

#249!5?H#116G#255???A@#159?@#194??A!27?A?@$#151!255?!255?!80?A

there seem to be bands of very different length here. #116G is what, the very first column? really? don't you need '$' between colors? no?

@dankamongmen
Copy link
Owner Author

mpv is just using libsixel, so let's take a look at that.

@dankamongmen
Copy link
Owner Author

finding out some very interesting things about libsixel...it seems to emit colors which it never uses? and they definitely reproduce colors within a band...very interesting.

@dankamongmen
Copy link
Owner Author

dankamongmen commented Mar 8, 2021

here's a big-ass chunk of libsixel's first encoded frame

#103A~Vlj\V|j\vlU|f|J}t^j}V|vmZ|Vmv\vi^uLzmZulV\f|J|vLzulV|Uj|VlyVlztmZvmtnZu^tnYnT~Uj|nQ|E~ShuLzU~TnYnlZvlv\RxiNzmDvU|R~UlraviXSLXv~iLfqvYHt^Tb]TlOlitgtgtlst\vs~d\XoeGThPUHzawfwhpDhITTLoLdHk`TWJPEpLoxDWlplTHThAlUjiTk@U@SdIDwDh?RGbGfGdCbC@GHSDNoMOdO@s@tIhFGTlHuTeHYhQHYJAPcHcXQHcHq`ScHCGB?]`[`ChAj?@?B_`CB?`O?D?dAW@_?sGM_CONoQcP_K?`O_??K`GSi?Rc@AgP?OdIO`?S@IL?SCHO?hQKP?CdOk@W_L?_D?KPCO@CAGA@_???_?OC?S_O?O_?O?__G???aC?CGA??G?_I_?IC??S?O!6?acA_a?O_A_!4?EG@???K?_E???G?CG?q?IG?b?BS@?Q?t@a???O_?O?oGcG__G???gCGC?CGcWCW!4?G???AO?a???O??Q?O??S???C?SO?AG!4?O???D??H?CAgACIGOG_@?s?S@KBO??C!4?BG@Q!4?@GAg@GA?`I@AHAC?SOC??W_CC?C!6?G??O!4?GA?A?E?AOA?ACI?A??@?GP_?@??_?_P??@$
#67T?gQCagASAGQhAWAs@I_S@gAGPcAgPGaGT_HqCPcHQgAWAsAGqCHQgAhSAgQDgQCIPcGPIOcH_IOdOi?hSAOl?x?JSHqCh?iOdOQcGQGaKAT_CPi?hAc?hAGTG@cbOeG?TaWDGdOI_IS`GQd?T?TAT?Q@I_?H?OaCJOtASi?s?\AODQKqS`g_OFoGq@EGdOcXCaLAOf?IOaSaSgQ_OC_OigSgQOgBOEsGeGo?QGQWa[aSbO_D_HQKi@]?dO_T_?Q?GOAcCGe?cWeGUPEgUPeGQHOaXCWp?I_CGQGSgCoCIO_Wa[@eOKOT_KRcH_@WP_?FGXELOHSLYD}@W`ADyCXSk@MwDGShE{@{c_ZAhQCjSC`Ish?G@scHOk@wGpCWdWrkTgsIdItGTgBW`GDiDAlaDQDQdQdO`W`O`S`SDY@SJO@YdIPETGTATG@?TGTIDIPKbChO`SiSHsBOJOHQhQdQPc@KPU@[?g?s`CTAGPiPeHU`E`CP?DIDQlOlO@oH_P?P?P?tGt?tOLQl_FWDiDaLY`Id?d?d?t?P?dGdOd?lODgAS?OcO_O?@O@?`A?OCa?A_?O??ePa@_XcO_OcGtISgQ_CO_Gs?OcW?S_X?DOdOD?Pg@gPkpEtAta\AdAlQDGtGtGpK`S`S`OdGtAc?a?C?cOCPC?C_Y$
#66g
#109???O
#73!4?_
#109!33?_
#66!41?A??_
#102A
#66!15?_??O???G??G??OC??S?G
#102a!4?O?G
#66??A!5?A?GQ?I?G?I?G?AGA?I?_???G??HA???G!5?CAI?G?AC???A?A??O??G??C?G???@?H?PG@?@?@?d?Cg?@?P?BGd?`?@?@?GI?IOE?@??_?OEGAGQc@a@o@?`O@?@?@_?_@??O@??HC?@?A`?@Q`?`?@OH_PC@?@?_O?@?_?O?OAS_C?CoGc?_?_E???_?_COg?@_?A@O_@A?`??@?A@?C_?c`O?HOc@AGdOI@Q?BCAd?GB?E???@???O?A_??cAS_?_OO?g?O??G?KOC??[?G?_??G_?GC??C?_?a?_aSO???__?_?OGOA?@?@???c??c??C??CO??_?_?AS@G?G_G_??CO??SGOGCO_?O!4?C?C?SGc?CG!4?G?C!4?O?_OOG?_?C?WOGOG?GG???O!6?OCHaC@ACHgOGgOC_O@O?GSHi?SHOG?cI?AGA?@???@?@G@GDO@A?A??@!4?gAG_CA?i?c??W?G???_O_??O_!7?g??O?G_?G?hC_XoI@A?qO?H?_$
#245!106?C!4?O!8?G
#8??A
#245!11?c??_G?_
#102?A???A?A
#245??A!8?C@?_!6?@
#8!5?A!9?G_!4?A???_?O?A?A!9?C
#61!13?C
#8???G?G?O
#60!8?_
#245!6?O_
#60???G!4?O???O
#245_
#96G??G???G
#61??C?A??a?C!8?A?A???I?C??G?a??A!6?A?I!6?A_!6?AC
#97!6?O??A
#61!6?A
#96!7?A
#97!5?A!5?A???A!5?A!7?O???A!4?A??G!9?G!6?G?_
#96!4?O???G?A
#97??_?_AO
#96!5?OA!6?_?A?G???A???G?_Q?_G??C@!8?A!4?A?C?A!8?C?G
#97!5?G_!7?G!9?O?G!5?G?G?G?G
#96!6?_!4?A??A?A
#243!13?A
#96???AO!4?a?A?_G!5?GDA?_@??O@?C
#102!5?PA?A???A??O?OA?C!8?G!5?_??G?C!8?OG??C?C??G?GA?AC$!160?A?GA??C_?C?C?ACAG?O??a?AO?Q?A?c?_G?ACA?A???_GAS!4?A?O?i?G?Q?AOIc?S?O?Q??OaCa?_O?@_GASI?I!4?A!7?AC!5?Q?A?C_A?CGA?Oa?C!5?O???CaCG?IO?GAK@iOA?DAC@?@?@I@??@?GA?OG?CG@A?A???G?G?C??G??CGAG???_!4?C!4?GO?A???C??G?OA??G?GO?S?S?C?C??_!10?G??O_???AO??_E!6?_?_???G??G?G@G??@!4?G??AC?O??ACA???G?c???_??C@??S?G?_AS@??CGC???G??C???O!4?cA?O?a??OA???Ic?SA!9?a???G??c?_Ao?oAG?_??QGO?G?_?OGO?Q?@???_?CaC?O!4?_?C@?OGOC???CG`AO_???O
#61?A??G???_???A!5?G??OA?A!8?GO??_!9?G!7?O
#97!4?G?A
#61???G
#243??_$
#8!165?A
#61!21?O
#96???G
#60!17?C
#97??A???A
#60!15?C
#8!25?GC!4?c?C
#60!11?_!7?O?G??C!16?G
#96!6?O!6?A
#60?O???_!7?O??A_?A??_Q?AO?_??Q?GQ?G?A_A!9?_!6?_!4?A?A!6?CO!4?O!4?O?G_G???A?GC?A??G?cA?CA??_G_G!4?C?A_AG?GOCA!4?G!4?G?GC_A!4?G???a??I?`!7?A!4?_!5?_G??A_!4?g!5?G_?c!8?OAG?A???_?CO???A???AC?GQ?EG??G_A@?G@I?H?CAcACQG@G_@Ah?CBGA??G!4?_@I?Q?_?ACOAc??A?dG@EGIc?g!4?O_?C!6?_AG_?A??GA?AOA_E_I?A_ICAOG??COI?A?G_G?@aC?C$
#8!255?!27?O???C???CO?O!6?cO!5?G??_?G!4?_!7?@??A@??O??@?C!6?C!8?O??@
#61!10?Q?A???A?O?A?A
#96!5?_???A?I!4?AG???A?AGA?A?A?A?G
#61?A!4?A!6?_!6?A?G???G
#97!8?GO!5?_??_???O!10?_??S?G?G_G?O??G???G?G
#96!24?_?G?C!7?CA
#61!4?A???A??_A!5?Q?A???_!5?A!7?CA?G!5?_!4?OC???C
#97!24?AGA?A?A!6?G
#96!25?A??A!7?A$
#245!255?!28?_G
#97??A?A!4?G?OC!5?A!7?GA?A!12?A??Q
#8!43?O!4?_??_?@??G@G`?PGBG@?@ID?L?D?D?D?DO@?`?b?@O@QDO@C@O@Q@_@AT?T???@GS?D?D?PC`?H?_S?o@O@O@?@Q@G@A@?O!14?P_HA@?`A@A@ADG@Q@O@A@O@A@?@a@?PA@?`Qh?L_DGD?@OD?`A@A@?DA@?`ADG@?dAd?DO!4?_?_?C@?@?@??GC@!5?_?C@?@?PCO_?C??GS?C?A?cGO?OcO_??h?@OD?@G@?@?PA`?T?T?L?DOd?T?DGP?P?@SH?`OD?d?O@?A@OcPC@CGO@Y$
#61!255?!196?AC!4?A!7?C!4?AG!7?A?A!4?C!8?A?A?O???A?A?A?A?A?A?A?A?A?A_
#97!33?G??_?C??O??O!14?G!4?A
#96!29?_!6?G?G?A???A???A???G
#243!15?A-

@dankamongmen
Copy link
Owner Author

Interestingly, the libsixel/mpv output appears to be much larger than our own now.

@dankamongmen
Copy link
Owner Author

we seem quite a bit faster now than we were, running e.g. ncplayer -bpixel -snone ../data/fm6.wmv. our color selection still sucks ass.

@dankamongmen
Copy link
Owner Author

i'm thinking of a new approach for colors (assuming we don't proceed with the idea in #1380):

  • break up a 4096-entry table
  • each corresponds to a 16-out-of-256 skip in one of the colors (161616 == 4096)
  • for each entry, store a count and a tripartite sum (for maximum safety, 32 bits for each component)
    • alternatively, divide as we go, but this is less desirable
  • match each color into its entry in O(1)
    • increase count
    • add to sums
  • at the end, take the 256 most populated entries, divide down their sums, and go

this is a 64KiB table assuming 4x32-bit components, totally doable.

@dankamongmen
Copy link
Owner Author

trying to take this to 32x32x32 would require half a meg, but allows us to skip in terms of 8

@dnkl
Copy link
Contributor

dnkl commented Mar 8, 2021

I'm staying on my current notcurses version for a little while longer, while doing some sixel optimizations in foot. After that I'll be happy to see how the latest ncplayer is faring in foot :)

@dankamongmen
Copy link
Owner Author

i took a look at matchColor() in @klamonte's Jexer. she's using an interesting method that makes use of HSL, as opposed to keeping things in RGB -- i'm not very familiar with HSL's place in color theory. is it a more easily searched space? either way, hslColors is set up in makePalette(). it's set up with what look to be linear gradiations based on the palette size.

ahhh, after reading up on HSL, yes, its value here is clear. we basically get our saturation and lightness for free, and then just have to find the closest hue. either way, this is a prepartition-and-match algorithm, very similar to what i'm describing above, except i'm proposing finer partitioning, and backfitting of the determined values. what does libsixel do?

@dankamongmen
Copy link
Owner Author

it looks like libsixel uses a wholly static palette to start with (take a look at pal_xterm256[]), but then extracts one dyanmically with sixel_quant_make_palette() using median cut, which is pretty canonical and a strong approach.

@dankamongmen
Copy link
Owner Author

octree is a valid method.

@dankamongmen
Copy link
Owner Author

both the algorithm i proposed, and that used by Jexer, are popularity algorithms. median cut seems a better approach, though perhaps it ought be united with an HSL conversion to get the best of both worlds.

@dankamongmen
Copy link
Owner Author

ahhh, libsixel first calculates a histogram to approximate the number of colors, and if they're few enough, doesn't generate an approximate palette. so it gets the exact palette in that case, just like we do.

@dankamongmen
Copy link
Owner Author

a kohonen neural net might also be a good method

@dankamongmen
Copy link
Owner Author

So when evaluating median cut v octrees v Kohonen nets, we desire:

  • to do a single pass -- we want to build up the actual sixels while building up the color table. this means we want to avoid any initial full palette extraction step that can't be mapped back to the resulting palette
  • continuity in reduction -- this is maybe just another, more general way of stating the above. each time we eliminate a color, either by folding it into another or merging two, we want to merge the associated sixels smoothly

@dankamongmen
Copy link
Owner Author

file:///home/dank/Downloads/Kohonen_neural_networks_for_optimal_colour_quantiz.pdf is the best read i've found on Kahonen-based quantization

@dankamongmen
Copy link
Owner Author

what if we had a constant MAXCOLOR-size array, and tracked the closest elements as we inserted, and just merged closest when full on a miss? there would be a lot of divisions....

@dankamongmen
Copy link
Owner Author

it would appear that the Rust color_quant crate uses Kohonen https://docs.rs/crate/color_quant/1.1.0

@dnkl
Copy link
Contributor

dnkl commented Mar 16, 2021

#1  0x00007ffff7f9d002 in sixel_blit (nc=0x5555555be3d0, linesize=3776, data=0x5555559ccf00, begy=0, begx=0, leny=531, lenx=944, 
    bargs=0x7fffffffdb20) at ../../src/lib/sixel.c:418
418       memset(stable.data, 0, sixelcount * bargs->pixel.colorregs);
(gdb) p sixelcount
$1 = 84016
(gdb) p bargs->pixel.colorregs
$2 = 1024
(gdb) p colorregs
$3 = 256
(gdb) p stable
$4 = {colors = 0, deets = 0x555555bb6770, data = 0x7fffee907010 "", table = 0x555555634be0 "\260\016\276\367\377\177", 
  sixelcount = 84016, colorregs = 256}

I.e. stable.data is 256*84016, but the memset() sets 1024*84016 bytes.

@dnkl
Copy link
Contributor

dnkl commented Mar 16, 2021

But it works in XTerm... hmm

@dnkl
Copy link
Contributor

dnkl commented Mar 16, 2021

But it works in XTerm... hmm

Ah, if I bump xterm*numColorRegisters from 256 to 1024, it crashes in XTerm too.

@dnkl
Copy link
Contributor

dnkl commented Mar 16, 2021

Given

  int colorregs = bargs->pixel.colorregs;
  if(colorregs > 256){
    colorregs = 256;
  }

This seems like the appropriate solution (and works):

diff --git a/src/lib/sixel.c b/src/lib/sixel.c
index 46169e7f8..9d117c593 100644
--- a/src/lib/sixel.c
+++ b/src/lib/sixel.c
@@ -415,8 +415,8 @@ int sixel_blit(ncplane* nc, int linesize, const void* data, int begy, int begx,
     return -1;
   }
   // stable.table doesn't need initializing; we start from the bottom
-  memset(stable.data, 0, sixelcount * bargs->pixel.colorregs);
-  memset(stable.deets, 0, sizeof(*stable.deets) * bargs->pixel.colorregs);
+  memset(stable.data, 0, sixelcount * colorregs);
+  memset(stable.deets, 0, sizeof(*stable.deets) * colorregs);
   if(extract_color_table(data, linesize, begy, begx, leny, lenx, &stable)){
     free(stable.table);
     free(stable.data);

@dankamongmen
Copy link
Owner Author

i wouldn't mess around with development branch stuff -- it's not generally ready for production -- but i'll certainly take quality investigations and bugfixes where i can find them =]. thanks, friend!

@dankamongmen
Copy link
Owner Author

i can similarly raise a coredump using -snone -bpixel ../data/penrose-tiling.png, in both kitty and xterm.

==3791751== Invalid write of size 4
==3791751==    at 0x4886D7C: plane_blit_sixel (internal.h:1104)
==3791751==    by 0x4886D7C: sixel_blit_inner (sixel.c:261)
==3791751==    by 0x4887341: sixel_blit (sixel.c:294)
==3791751==    by 0x4851EB3: rgba_blit_dispatch (internal.h:1261)
==3791751==    by 0x4851EB3: ffmpeg_blit(ncvisual*, int, int, ncplane*, blitset const*, int, int, int, int, blitterargs const*) (ffmpeg.cpp:525)
==3791751==    by 0x488A4D4: ncvisual_blit (visual.cpp:21)
==3791751==    by 0x488B0D5: ncvisual_render_pixels(tinfo*, ncvisual*, blitset const*, int, int, int, int, ncplane*, ncscale_e, ncplane*) (visual.cpp:533)
==3791751==    by 0x488B550: ncvisual_render (visual.cpp:589)
==3791751==    by 0x4851AD1: ffmpeg_stream(notcurses*, ncvisual*, float, int (*)(ncvisual*, ncvisual_options*, timespec const*, void*), ncvisual_options const*, void*) (ffmpeg.cpp:410)
==3791751==    by 0x10BBED: stream (Visual.hh:76)
==3791751==    by 0x10BBED: main (play.cpp:388)
==3791751==  Address 0xeb30e80 is 0 bytes after a block of size 78,080 alloc'd
==3791751==    at 0x483AB65: calloc (vg_replace_malloc.c:760)
==3791751==    by 0x4874CEB: ncplane_new_internal (notcurses.c:312)
==3791751==    by 0x4876803: create_initial_ncplane (notcurses.c:402)
==3791751==    by 0x4876803: notcurses_core_init (notcurses.c:1033)
==3791751==    by 0x484A76E: ncpp::NotCurses::NotCurses(notcurses_options const&, _IO_FILE*) (NotCurses.cc:37)
==3791751==    by 0x10BA50: main (play.cpp:349)
==3791751== 
==3791751== Invalid write of size 1
==3791751==    at 0x4886D7F: plane_blit_sixel (internal.h:1105)
==3791751==    by 0x4886D7F: sixel_blit_inner (sixel.c:261)
==3791751==    by 0x4887341: sixel_blit (sixel.c:294)
==3791751==    by 0x4851EB3: rgba_blit_dispatch (internal.h:1261)
==3791751==    by 0x4851EB3: ffmpeg_blit(ncvisual*, int, int, ncplane*, blitset const*, int, int, int, int, blitterargs const*) (ffmpeg.cpp:525)
==3791751==    by 0x488A4D4: ncvisual_blit (visual.cpp:21)
==3791751==    by 0x488B0D5: ncvisual_render_pixels(tinfo*, ncvisual*, blitset const*, int, int, int, int, ncplane*, ncscale_e, ncplane*) (visual.cpp:533)
==3791751==    by 0x488B550: ncvisual_render (visual.cpp:589)
==3791751==    by 0x4851AD1: ffmpeg_stream(notcurses*, ncvisual*, float, int (*)(ncvisual*, ncvisual_options*, timespec const*, void*), ncvisual_options const*, void*) (ffmpeg.cpp:410)
==3791751==    by 0x10BBED: stream (Visual.hh:76)
==3791751==    by 0x10BBED: main (play.cpp:388)
==3791751==  Address 0xeb30e85 is 5 bytes after a block of size 78,080 alloc'd
==3791751==    at 0x483AB65: calloc (vg_replace_malloc.c:760)
==3791751==    by 0x4874CEB: ncplane_new_internal (notcurses.c:312)
==3791751==    by 0x4876803: create_initial_ncplane (notcurses.c:402)
==3791751==    by 0x4876803: notcurses_core_init (notcurses.c:1033)
==3791751==    by 0x484A76E: ncpp::NotCurses::NotCurses(notcurses_options const&, _IO_FILE*) (NotCurses.cc:37)
==3791751==    by 0x10BA50: main (play.cpp:349)

@dankamongmen
Copy link
Owner Author

this might just be from a huge allocation. -snone on this 4Kx4K image yields 2.8 meagsixels. by 256 colors gets...nah, just 715MB. that oughtn't be a problem (not a functional one on my workstation anyway).

@dankamongmen
Copy link
Owner Author

we blow up at y: 178 x: 372

@dankamongmen
Copy link
Owner Author

y: 178 x: 372 leny: 179 lenx: 373 innnnnnnnnteresting

@dankamongmen
Copy link
Owner Author

y: 178 x: 372 leny: 179 lenx: 373 dimy: 61 dimx: 80 ahhhh there's our problem

@dankamongmen
Copy link
Owner Author

the coredump is remedied

dankamongmen added a commit that referenced this issue Mar 17, 2021
dankamongmen added a commit that referenced this issue Mar 17, 2021
dankamongmen added a commit that referenced this issue Mar 17, 2021
dankamongmen added a commit that referenced this issue Mar 17, 2021
@dankamongmen dankamongmen added the bitmaps bitmapped graphics (sixel, kitty, mmap) label Mar 24, 2021
@dankamongmen dankamongmen modified the milestones: 3.0.0, 2.3.0 Apr 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bitmaps bitmapped graphics (sixel, kitty, mmap) bug Something isn't working perf sweet sweet perf
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants