8000 Fontmake is slow · Issue #367 · googlefonts/fontmake · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fontmake is slow #367

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

Closed
khaledhosny opened this issue Oct 28, 2017 · 58 comments
Closed

Fontmake is slow #367

khaledhosny opened this issue Oct 28, 2017 · 58 comments

Comments

@khaledhosny
Copy link
Collaborator

Building Mada using fontmake takes about ~15 minutes on my (not terribly slow) machine. I can live with some slowness, but 15 minutes is a bit too much. It was actually ~5 minutes before aliftype/mada@a367974 (5 minutes is still a bit too much as well).

@anthrotype
Copy link
Member

Yes, we know it's very slow.
For my part, I'm mostly focusing on correctness and feature-completeness more than performance at this stage. But we definitely plan on working towards making it faster.
It would be great if you could help us identify where the bottlenecks really are.

@khaledhosny
Copy link
Collaborator Author

It seems to that interpolation is the slowest part, based on the fact that generating variable font is several times faster than generating static instances.

@chrissimpkins
Copy link
Member
chrissimpkins commented Nov 1, 2017

cProfile run on UFO to ttf build in Python 3.6.3 with the FontProject.run_from_ufos() method, current fontmake master branch 4d10eb2.

Ranked by cumulative time over top 50 function calls:

         25301043 function calls (25114610 primitive calls) in 16.615 seconds

   Ordered by: cumulative time
   List reduced from 2074 to 50 due to restriction <50>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000   16.513   16.513 font_project.py:431(run_from_ufos)
        1    0.000    0.000   16.476   16.476 font_project.py:172(build_ttfs)
        4    0.000    0.000   16.475    4.119 loggingTools.py:369(wrapper)
        1    0.017    0.017   11.306   11.306 font_project.py:96(remove_overlaps)
     1567    0.051    0.000   10.227    0.007 booleanOperationManager.py:102(union)
     1567    0.052    0.000   10.176    0.006 booleanOperationManager.py:62(_performOperation)
     1567    0.006    0.000    7.465    0.005 booleanOperationManager.py:64(<listcomp>)
     2878    0.024    0.000    7.454    0.003 flatten.py:65(__init__)
     5756    0.075    0.000    7.075    0.001 flatten.py:421(_convertPointsToSegments)
    51828    0.148    0.000    6.991    0.000 flatten.py:142(__init__)
     9606    0.493    0.000    5.256    0.001 flatten.py:1141(_flattenSegment)
  1162553    0.864    0.000    3.628    0.000 flatten.py:1176(_pointOnLine)
  3583719    2.451    0.000    2.733    0.000 flatten.py:1172(_distance)
        1    0.000    0.000    2.075    2.075 font_project.py:147(convert_curves)
        1    0.000    0.000    2.075    2.075 ufo.py:277(font_to_quadratic)
        1    0.012    0.012    2.075    2.075 ufo.py:218(fonts_to_quadratic)
     1567    0.034    0.000    2.046    0.001 ufo.py:155(_glyphs_to_quadratic)
     9408    0.011    0.000    1.677    0.000 font.py:214(__iter__)
        1    0.002    0.002    1.668    1.668 font_project.py:116(decompose_glyphs)
    10982    0.006    0.000    1.661    0.000 layer.py:259(__getitem__)
     1567    0.017    0.000    1.655    0.001 layer.py:172(loadGlyph)
     1548    0.022    0.000    1.483    0.001 ufo.py:117(_set_segments)
        1    0.001    0.001    1.426    1.426 font_project.py:225(save_otfs)
        1    0.000    0.000    1.379    1.379 __init__.py:50(compileTTF)
    11512    0.085    0.000    1.350    0.000 pointPen.py:89(endPath)
393611/344465    0.276    0.000    1.325    0.000 base.py:206(postNotification)
    11512    0.136    0.000    1.240    0.000 pointPen.py:151(_flushContour)
   131158    0.073    0.000    1.232    0.000 glyphObjectPointPen.py:25(addPoint)
   131158    0.191    0.000    1.158    0.000 contour.py:575(addPoint)
410879/350221    0.775    0.000    1.151    0.000 notifications.py:117(postNotification)
5756/2878    0.012    0.000    1.109    0.000 pointPen.py:239(closePath)
5756/2878    0.033    0.000    1.102    0.000 pointPen.py:212(_flushContour)
    11512    0.399    0.000    1.084    0.000 contour.py:592(drawPoints)
5756/2878    0.011    0.000    1.066    0.000 pointPen.py:314(endPath)
201234/170905    0.151    0.000    1.003    0.000 base.py:379(_set_dirty)
     2878    0.004    0.000    0.992    0.000 pointPen.py:396(endPath)
     2878    0.045    0.000    0.988    0.000 pointPen.py:342(_flushContour)
   682408    0.902    0.000    0.918    0.000 flatten.py:1207(_getCubicPoint)
     4682    0.010    0.000    0.908    0.000 glyph.py:448(draw)
     2878    0.106    0.000    0.896    0.000 flatten.py:969(drawPoints)
     4682    0.009    0.000    0.886    0.000 glyph.py:456(drawPoints)
   131158    0.208    0.000    0.859    0.000 contour.py:172(insertPoint)
    25914    0.812    0.000    0.857    0.000 flatten.py:1118(_checkFlatPoints)
     4701    0.034    0.000    0.787    0.000 glifLib.py:218(getGLIF)
     1567    0.005    0.000    0.780    0.000 layer.py:585(_stampGlyphDataState)
     1567    0.004    0.000    0.770    0.000 glifLib.py:266(readGlyph)
     1567    0.003    0.000    0.755    0.000 glifLib.py:252(getGLIFModificationTime)
     1594    0.704    0.000    0.705    0.000 {built-in method io.open}
     8634    0.015    0.000    0.644    0.000 glyphObjectPointPen.py:19(endPath)
    25914    0.014    0.000    0.637    0.000 flatten.py:1089(_scalePoints)

Ranked by internal time over top 50 function calls:

         25301055 function calls (25114616 primitive calls) in 16.720 seconds

   Ordered by: internal time
   List reduced from 2074 to 50 due to restriction <50>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  3583719    2.455    0.000    2.737    0.000 flatten.py:1172(_distance)
   682408    0.923    0.000    0.940    0.000 flatten.py:1207(_getCubicPoint)
  1162553    0.871    0.000    3.640    0.000 flatten.py:1176(_pointOnLine)
    25914    0.809    0.000    0.854    0.000 flatten.py:1118(_checkFlatPoints)
410879/350221    0.789    0.000    1.171    0.000 notifications.py:117(postNotification)
     1594    0.703    0.000    0.703    0.000 {built-in method io.open}
     2878    0.502    0.000    0.631    0.000 flatten.py:463(<listcomp>)
     9606    0.494    0.000    5.295    0.001 flatten.py:1141(_flattenSegment)
    11512    0.409    0.000    1.136    0.000 contour.py:592(drawPoints)
    25914    0.350    0.000    0.625    0.000 flatten.py:1095(<listcomp>)
     1567    0.339    0.000    0.339    0.000 {method 'Execute' of 'pyclipper.Pyclipper' objects}
  1386702    0.321    0.000    0.321    0.000 {built-in method builtins.round}
  3583719    0.282    0.000    0.282    0.000 {built-in method math.sqrt}
393611/344465    0.279    0.000    1.349    0.000 base.py:206(postNotification)
  2618938    0.227    0.000    0.227    0.000 {method 'append' of 'list' objects}
     2878    0.226    0.000    0.253    0.000 flatten.py:654(reCurveSubSegments)
   131158    0.212    0.000    0.877    0.000 contour.py:172(insertPoint)
   743666    0.201    0.000    0.238    0.000 base.py:69(_get_dispatcher)
   131158    0.194    0.000    1.180    0.000 contour.py:575(addPoint)
     2904    0.179    0.000    0.274    0.000 flatten.py:498(reCurveFromEntireInputContour)
     2878    0.175    0.000    0.175    0.000 booleanOperationManager.py:59(<listcomp>)
     2878    0.156    0.000    0.233    0.000 glifLib.py:1178(_validateAndMassagePointStructures)
201234/170905    0.153    0.000    1.020    0.000 base.py:379(_set_dirty)
    51828    0.152    0.000    7.036    0.000 flatten.py:142(__init__)
    11512    0.144    0.000    1.258    0.000 pointPen.py:151(_flushContour)
   656711    0.142    0.000    0.142    0.000 flatten.py:1016(__init__)
     1567    0.110    0.000    0.110    0.000 {method 'feed' of 'xml.etree.ElementTree.XMLParser' objects}
     2878    0.110    0.000    0.931    0.000 flatten.py:969(drawPoints)
  1262592    0.103    0.000    0.103    0.000 {built-in method builtins.abs}
     5756    0.098    0.000    0.515    0.000 pointPen.py:271(_flushContour)
   131158    0.095    0.000    0.095    0.000 point.py:12(__init__)
     9606    0.095    0.000    0.311    0.000 flatten.py:1180(_estimateCubicCurveLength)
    45126    0.094    0.000    0.098    0.000 glyphObjectPointPen.py:64(addPoint)
    95193    0.092    0.000    0.123    0.000 base.py:336(_destroyRepresentationsForNotification)
    11512    0.088    0.000    1.370    0.000 pointPen.py:89(endPath)
   131158    0.079    0.000    1.259    0.000 glyphObjectPointPen.py:25(addPoint)
     5756    0.077    0.000    7.121    0.001 flatten.py:421(_convertPointsToSegments)
     1548    0.066    0.000    0.170    0.000 _g_l_y_f.py:605(compileDeltasGreedy)
597028/578876    0.066    0.000    0.076    0.000 {built-in method builtins.len}
   125823    0.057    0.000    0.076    0.000 _g_l_y_f.py:1202(__getitem__)
    26695    0.054    0.000    0.055    0.000 notifications.py:52(addObserver)
    26695    0.053    0.000    0.055    0.000 notifications.py:97(removeObserver)
     1567    0.053    0.000   10.298    0.007 booleanOperationManager.py:62(_performOperation)
     1567    0.053    0.000   10.351    0.007 booleanOperationManager.py:102(union)
   167850    0.052    0.000    0.068    0.000 pointPen.py:136(addPoint)
    45126    0.050    0.000    0.069    0.000 flatten.py:345(addPoint)
   393474    0.048    0.000    0.048    0.000 point.py:77(_get_identifier)
     2878    0.045    0.000    0.975    0.000 pointPen.py:342(_flushContour)
       57    0.044    0.001    0.045    0.001 <frozen importlib._bootstrap_external>:830(get_data)
     5756    0.044    0.000    0.056    0.000 {pyclipper.Orientation}

@chrissimpkins
Copy link
Member
chrissimpkins commented Nov 1, 2017

cProfile run on UFO to otf build in Python 3.6.3 with the FontProject.run_from_ufos() method, current fontmake master branch 4d10eb2.

Ranked by cumulative time over top 50 function calls:

         26123163 function calls (26024827 primitive calls) in 16.939 seconds

   Ordered by: cumulative time
   List reduced from 2160 to 50 due to restriction <50>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000   16.832   16.832 font_project.py:431(run_from_ufos)
        1    0.000    0.000   16.800   16.800 font_project.py:162(build_otfs)
        3    0.000    0.000   16.800    5.600 loggingTools.py:369(wrapper)
        1    0.019    0.019   11.650   11.650 font_project.py:96(remove_overlaps)
     1567    0.054    0.000   10.511    0.007 booleanOperationManager.py:102(union)
     1567    0.054    0.000   10.457    0.007 booleanOperationManager.py:62(_performOperation)
     1567    0.007    0.000    7.650    0.005 booleanOperationManager.py:64(<listcomp>)
     2878    0.026    0.000    7.639    0.003 flatten.py:65(__init__)
     5756    0.077    0.000    7.241    0.001 flatten.py:421(_convertPointsToSegments)
    51828    0.150    0.000    7.155    0.000 flatten.py:142(__init__)
     9606    0.505    0.000    5.348    0.001 flatten.py:1141(_flattenSegment)
  1162553    0.878    0.000    3.686    0.000 flatten.py:1176(_pointOnLine)
        1    0.000    0.000    3.229    3.229 font_project.py:225(save_otfs)
  3583719    2.492    0.000    2.778    0.000 flatten.py:1172(_distance)
        1    0.000    0.000    2.337    2.337 __init__.py:18(compileOTF)
        2    0.000    0.000    2.175    1.088 __init__.py:192(save)
    26/20    0.000    0.000    2.168    0.108 __init__.py:647(_writeTable)
       20    0.000    0.000    2.163    0.108 __init__.py:665(getTableData)
     9408    0.011    0.000    1.929    0.000 font.py:214(__iter__)
        1    0.002    0.002    1.920    1.920 font_project.py:116(decompose_glyphs)
     9415    0.006    0.000    1.912    0.000 layer.py:259(__getitem__)
     1567    0.021    0.000    1.906    0.001 layer.py:172(loadGlyph)
        2    0.000    0.000    1.586    0.793 C_F_F_.py:18(compile)
        2    0.000    0.000    1.586    0.793 __init__.py:92(compile)
     4701    0.007    0.000    1.519    0.000 psCharStrings.py:981(calcBounds)
     4701    0.011    0.000    1.497    0.000 psCharStrings.py:974(draw)
     4701    0.207    0.000    1.441    0.000 psCharStrings.py:282(execute)
        1    0.000    0.000    1.305    1.305 postProcessor.py:12(__init__)
        2    0.005    0.003    0.990    0.495 __init__.py:2322(recalcFontBBox)
     2878    0.110    0.000    0.956    0.000 flatten.py:969(drawPoints)
     8634    0.308    0.000    0.938    0.000 contour.py:592(drawPoints)
   682408    0.917    0.000    0.934    0.000 flatten.py:1207(_getCubicPoint)
     4701    0.038    0.000    0.908    0.000 glifLib.py:218(getGLIF)
     1567    0.005    0.000    0.900    0.001 layer.py:585(_stampGlyphDataState)
     1567    0.004    0.000    0.884    0.001 glifLib.py:266(readGlyph)
    25914    0.827    0.000    0.873    0.000 flatten.py:1118(_checkFlatPoints)
     1567    0.004    0.000    0.872    0.001 glifLib.py:252(getGLIFModificationTime)
    90249    0.053    0.000    0.870    0.000 glyphObjectPointPen.py:25(addPoint)
262867/236745    0.188    0.000    0.867    0.000 base.py:206(postNotification)
    90249    0.135    0.000    0.817    0.000 contour.py:575(addPoint)
     1594    0.816    0.001    0.816    0.001 {built-in method io.open}
     3115    0.007    0.000    0.741    0.000 glyph.py:448(draw)
     3115    0.006    0.000    0.726    0.000 glyph.py:456(drawPoints)
271501/239623    0.514    0.000    0.716    0.000 notifications.py:117(postNotification)
     1567    0.004    0.000    0.704    0.000 glifLib.py:823(_readGlyphFromTree)
     1567    0.013    0.000    0.699    0.000 glifLib.py:843(_readGlyphFromTreeFormat1)
    25914    0.015    0.000    0.690    0.000 flatten.py:1089(_scalePoints)
    25914    0.376    0.000    0.676    0.000 flatten.py:1095(<listcomp>)
     1567    0.003    0.000    0.649    0.000 booleanOperationManager.py:72(<listcomp>)
     2878    0.006    0.000    0.646    0.000 flatten.py:458(__init__)

Ranked by internal time over top 50 function calls:

         26123179 function calls (26024843 primitive calls) in 16.069 seconds

   Ordered by: internal time
   List reduced from 2162 to 50 due to restriction <50>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  3583719    2.435    0.000    2.716    0.000 flatten.py:1172(_distance)
   682408    0.906    0.000    0.922    0.000 flatten.py:1207(_getCubicPoint)
  1162553    0.858    0.000    3.605    0.000 flatten.py:1176(_pointOnLine)
    25914    0.807    0.000    0.852    0.000 flatten.py:1118(_checkFlatPoints)
     1594    0.715    0.000    0.715    0.000 {built-in method io.open}
     2878    0.521    0.000    0.647    0.000 flatten.py:463(<listcomp>)
     9606    0.494    0.000    5.238    0.001 flatten.py:1141(_flattenSegment)
271501/239623    0.456    0.000    0.648    0.000 notifications.py:117(postNotification)
    25914    0.358    0.000    0.627    0.000 flatten.py:1095(<listcomp>)
     1567    0.343    0.000    0.343    0.000 {method 'Execute' of 'pyclipper.Pyclipper' objects}
  1414542    0.317    0.000    0.317    0.000 {built-in method builtins.round}
     8634    0.290    0.000    0.887    0.000 contour.py:592(drawPoints)
  3583803    0.281    0.000    0.281    0.000 {built-in method math.sqrt}
   277371    0.255    0.000    0.477    0.000 psCharStrings.py:1046(getToken)
  2547697    0.223    0.000    0.223    0.000 {method 'append' of 'list' objects}
     2878    0.220    0.000    0.246    0.000 flatten.py:654(reCurveSubSegments)
     4701    0.180    0.000    1.311    0.000 psCharStrings.py:282(execute)
262867/236745    0.179    0.000    0.792    0.000 base.py:206(postNotification)
     2904    0.175    0.000    0.267    0.000 flatten.py:498(reCurveFromEntireInputContour)
     2878    0.167    0.000    0.242    0.000 glifLib.py:1178(_validateAndMassagePointStructures)
    51828    0.148    0.000    6.975    0.000 flatten.py:142(__init__)
     2878    0.142    0.000    0.142    0.000 booleanOperationManager.py:59(<listcomp>)
     3134    0.141    0.000    0.483    0.000 psCharStrings.py:994(compile)
    90249    0.140    0.000    0.578    0.000 contour.py:172(insertPoint)
   517113    0.139    0.000    0.166    0.000 base.py:69(_get_dispatcher)
   656711    0.139    0.000    0.139    0.000 flatten.py:1016(__init__)
    90249    0.130    0.000    0.781    0.000 contour.py:575(addPoint)
     1567    0.118    0.000    0.118    0.000 {method 'feed' of 'xml.etree.ElementTree.XMLParser' objects}
     2878    0.106    0.000    0.891    0.000 flatten.py:969(drawPoints)
   257449    0.104    0.000    0.104    0.000 {built-in method builtins.min}
757713/757712    0.100    0.000    0.104    0.000 {built-in method builtins.isinstance}
134423/118484    0.095    0.000    0.593    0.000 base.py:379(_set_dirty)
     9606    0.093    0.000    0.304    0.000 flatten.py:1180(_estimateCubicCurveLength)
     1567    0.093    0.000    0.106    0.000 specializer.py:268(specializeCommands)
  1162670    0.091    0.000    0.091    0.000 {built-in method builtins.abs}
806340/792551    0.082    0.000    0.088    0.000 {built-in method builtins.len}
   146266    0.079    0.000    0.139    0.000 psCharStrings.py:179(encodeInt)
     5756    0.075    0.000    7.058    0.001 flatten.py:421(_convertPointsToSegments)
   121388    0.072    0.000    0.223    0.000 arrayTools.py:29(updateBounds)
    45126    0.070    0.000    0.074    0.000 glyphObjectPointPen.py:64(addPoint)
   242574    0.068    0.000    0.068    0.000 py23.py:102(bytechr)
    90249    0.063    0.000    0.063    0.000 point.py:12(__init__)
     5756    0.063    0.000    0.325    0.000 pointPen.py:151(_flushContour)
   242137    0.056    0.000    0.086    0.000 py23.py:104(byteord)
   254085    0.053    0.000    0.054    0.000 {built-in method builtins.max}
    90249    0.052    0.000    0.833    0.000 glyphObjectPointPen.py:25(addPoint)
     1567    0.052    0.000   10.156    0.006 booleanOperationManager.py:62(_performOperation)
     1567    0.051    0.000   10.207    0.007 booleanOperationManager.py:102(union)
    45126    0.050    0.000    0.068    0.000 flatten.py:345(addPoint)
153554/151812    0.049    0.000    0.057    0.000 {built-in method builtins.getattr}

@chrissimpkins
Copy link
Member

@khaledhosny do you have a smaller (than Mada) body of source that you build from a designspace (or is it possible to push one)? Happy to add data on run_from_glyphs() and run_from_designspace() as well so that there is a starting point for this work.

@khaledhosny
Copy link
Collaborator Author

There is https://github.com/alif-type/reem-kufi, but it does not do any interpolation which I think is the interesting difference. But I can run with cProfile myself. I’ll add my results here.

@graphicore
Copy link

@davelab6 just asked me if we could run fontmake in parallel on the Google Cloud Platform, since I'm doing something similar for Font Bakery right now.

Sure, we could, it would need some modifications though, but we could reuse or extend what we have. It would probably build one instance per pod. Similarly, one quick improvement on a local machine would be to just spawn one fontmake process per instance to create. The kernel would take care of distributing this to different processor cores.

It would be nice though, to run the work within an instance build in parallel, i.e. map per glyph, interpolate and then reduce to the new font, on a multiprocessor machine that would be awesome.

Still, my guess is that something fishy is going on in that code, I believe it should be much faster judging from what it does. So, a thorough performance review may help as well.

It would be great if you could help us identify where the bottlenecks really are.

Yeah, that's the hard part :-) Usually it's either moving calls to functions out of a loop, to execute just before or to do some memoization (=buy speed with memory) etc. Caching is another form of hell though.

The problem is, a performance review is time-consuming since you need to get to know the code very well. About profiling to identify bottlenecks, I remember reading a blog post (which I can't find anymore of course). The author used to interrupt the running program at random points in time and claimed that the performance critical code would be very often where the program was at that moment, because that's where most time is spent. So he would take a deep look at these common locations and start optimizing there. This is a bit brutal and dirty, but not a really bad idea. For python, this would mean to start fontmake and then hit Ctrl+c while it's running. This will produce a KeyboardInterrupt with a full stack trace. I never tried this, but I would give it a shot in this case 😁

@anthrotype
Copy link
Member

for parallelizing python programs that do CPU intensive stuff (non I/O intensive) one cannot use multi-threading because of the infamous CPython's global interpreter lock (GIL). So one has to use multiprocessing. But unlike threads, sub-processes by default don't share memory with the parent process and communication between them may require serialization and deserialization (read pickle); plus, on platforms that don't have fork like windows spawning new process is relatively expensive.
But yes, we definitely need to look into that at some point.

@anthrotype
Copy link
Member

because it is truly a waste that 7 of my 8 cores sit idly in the background while fontmake is crunching my fonts.

@graphicore
Copy link

Yeah, for now, would it be possible to just start say 8 processes of fontmake by hand, to build different instances of the same project, with a shell-script or so? Or would they race for the same intermediate folders on the harddrive?

So one has to use multiprocessing. But unlike threads, sub-processes by default don't share memory with the parent process and communication between them may require serialization and deserialization (read pickle)

Sure, maybe not so bad though, since shared memory can cause problems as well.

@anthrotype
Copy link
Member

Yeah, for now, would it be possible to just start say 8 processes of fontmake by hand, to build different instances of the same project

yes, if you have one UFO for each output font

$ fontmake -u MyFont-Regular.ufo -o ttf
$ fontmake -u MyFont-Bold.ufo -o ttf

If you have interpolated instances to make, you could run fontmake -m MyFont.designspace -o ufo first, and then run fontmake on each of the generated instance_ufo/.

@chrissimpkins
Copy link
Member
chrissimpkins commented Nov 1, 2017

Maybe something along these lines (Py3)?

from multiprocessing import Pool
from fontmake.font_project import FontProject

def build_fonts(ufo_path):
    fp = FontProject()
    fp.run_from_ufos(ufo_path, output=('ttf', 'otf'))

if __name__ == '__main__':
    with Pool(4) as p:
        p.map(build_fonts, ["UFOPATH1", "UFOPATH2", "UFOPATH3", "UFOPATH4"])

The standard output/logging will get ugly I suspect but I believe that would do the trick to build in four parallel processes. Filesystem access across processes possibly a problem too? Not somewhere where I can test at the moment.

Perhaps parallel builds of ttf and otf would be of benefit too? I don't know how much shared compilation happens there. If that works, the build script can be modified to accept arguments for the source paths to replace the ufo path list.

@chrissimpkins
Copy link
Member

Removal of the logger and timer may be low hanging fruit in production code too. I can look at this tonight if you haven't previously assessed. Isn't immediately clear to me what the timer is doing.

@anthrotype
Copy link
Member

the timer does nothing else than timing. It doesn't uses resources. You see it in the profiler because it wraps the other method calls as a decorator.

@anthrotype
Copy link
Member
anthrotype commented Nov 1, 2017

I suggest you simply redirect stderr to /dev/null for now. We'll have to rethink the whole logging interface in a multiprocessing fontmake.

@chrissimpkins
Copy link
Member

@anthrotype wondering if the decorated method calls are leading to this issue:

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
4    0.000    0.000   16.475    4.119 loggingTools.py:369(wrapper)

@anthrotype
Copy link
Member

which issue?

@anthrotype
Copy link
Member

as you can see from the legend, the total time (tottime) spent in that loggingTools decorator excluding the time made in calls to sub-functions is 0.000. The cumtime is the cumulative time spent in this and all subfunctions.

@chrissimpkins
Copy link
Member

Misread the percall columns. Sorry!

@chrissimpkins
Copy link
Member

What project is flatten.py located in?

@anthrotype
Copy link
Member

actually to mute the fontmake logger while multiprocessing, instead of redirecting stderr to /dev/null, you can just configure the global logging verbosity with a level of "ERROR" or "CRITICAL", e.g. logging.basicConfig(level="ERROR").

BTW, I noted this elsewhere, the fact fontmake's FontProject class takes a "verbose" argument to configure logging is a lie: the basicConfig function only works the first time is run, as the whole logging settings are global, not class or instance specific.

@chrissimpkins
Copy link
Member

@graphicore This might get you the line by line profiling that you need once a bit more digging has been performed at a higher level?

https://github.com/rkern/line_profiler

@anthrotype
Copy link
Member
anthrotype commented Nov 1, 2017

What project is flatten.py located in?

booleanOperations

it's not a chance that that is one of the most computationally heavy tasks

@chrissimpkins
Copy link
Member

Cython possible there?

@anthrotype
Copy link
Member
anthrotype commented Nov 1, 2017

Cython is already used for the clipping. Pyclipper is a Cython wrapper for the C++ Clipper library. BooleanOperations is a python wrapper that "flattens" (converts curves into polygons) the contours, passes them on to pyclipper, and re-curves the output polygons back into beziers.

I'm afraid there's no easy shortcut here, Chris.

@khaledhosny
Copy link
Collaborator Author

I can use a Makefile for this kind of parallelism, but it still wouldn’t help me that much since I build from design space not from individual UFOs. Let me try to interpolate then build the OTFs and see if it saves me much.

@khaledhosny
Copy link
Collaborator Author

Building OTF instances ordered by cumulative time:

         1150923135 function calls (1136399819 primitive calls) in 920.033 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.018    0.018  920.085  920.085 /fontmake/font_project.py:380(run_from_designspace)
        1    0.015    0.015  463.045  463.045 /mutatorMath/ufo/__init__.py:25(build)
        1    0.000    0.000  456.456  456.456 /mutatorMath/ufo/document.py:437(process)
        1    0.000    0.000  456.456  456.456 /mutatorMath/ufo/document.py:607(readInstances)
       12    0.108    0.009  456.456   38.038 /mutatorMath/ufo/document.py:619(_readSingleInstanceElement)
        1    0.000    0.000  451.928  451.928 /fontmake/font_project.py:436(run_from_ufos)
        1    0.000    0.000  451.304  451.304 /fontmake/font_project.py:167(build_otfs)
     63/3    0.013    0.000  451.304  150.435 /fontTools/misc/loggingTools.py:369(wrapper)
    27096    1.188    0.000  397.487    0.015 /mutatorMath/ufo/instance.py:307(addGlyph)
    27096    0.973    0.000  375.808    0.014 /mutatorMath/ufo/instance.py:348(_calculateGlyph)
    27120    1.721    0.000  307.599    0.011 /mutatorMath/objects/mutator.py:18(buildMutator)
  1057680    3.498    0.000  286.023    0.000 /mutatorMath/objects/mutator.py:188(getInstance)
1003440/949200    2.885    0.000  263.979    0.000 /mutatorMath/objects/mutator.py:212(makeInstance)
        1    0.007    0.007  255.994  255.994 /fontmake/font_project.py:230(save_otfs)
   488160    2.244    0.000  248.304    0.001 /mutatorMath/objects/bender.py:103(__call__)
   976320    1.508    0.000  245.247    0.000 /mutatorMath/objects/bender.py:10(__call__)
       12    0.000    0.000  242.322   20.194 /ufo2ft/__init__.py:18(compileOTF)
  1057680   20.330    0.000  226.046    0.000 /mutatorMath/objects/mutator.py:225(getFactors)
        1    0.399    0.399  157.324  157.324 /fontmake/font_project.py:101(remove_overlaps)
    27096    1.090    0.000  146.787    0.005 /booleanOperations/booleanOperationManager.py:102(union)
    27096    1.063    0.000  145.697    0.005 /booleanOperations/booleanOperationManager.py:62(_performOperation)
       12    0.001    0.000  113.140    9.428 /ufo2ft/postProcessor.py:20(process)
       12    0.000    0.000  112.831    9.403 /compreffor/__init__.py:74(compress)
       12    0.054    0.005  112.818    9.402 /compreffor/cxxCompressor.py:191(compreff)
       12  103.825    8.652  103.825    8.652 {compreffor._compreffor.compreff}
    27096    0.162    0.000   97.182    0.004 /booleanOperations/booleanOperationManager.py:64(<listcomp>)
    56232    0.522    0.000   96.923    0.002 /booleanOperations/flatten.py:65(__init__)
    27120    0.183    0.000   96.613    0.004 /mutatorMath/objects/mutator.py:33(<listcomp>)
  4881600   13.918    0.000   92.888    0.000 /mutatorMath/objects/mutator.py:248(_accumulateFactors)
       24    0.001    0.000   90.943    3.789 /fontTools/ttLib/__init__.py:192(save)
  360/288    0.003    0.000   90.838    0.315 /fontTools/ttLib/__init__.py:647(_writeTable)
      288    0.003    0.000   90.741    0.315 /fontTools/ttLib/__init__.py:665(getTableData)
   112464    1.734    0.000   89.248    0.001 /booleanOperations/flatten.py:421(_convertPointsToSegments)
   842256    3.070    0.000   87.380    0.000 /booleanOperations/flatten.py:142(__init__)
       12    0.000    0.000   77.761    6.480 /ufo2ft/postProcessor.py:12(__init__)
   175741    6.912    0.000   67.686    0.000 /booleanOperations/flatten.py:1141(_flattenSegment)
7937681/6096243    7.245    0.000   64.469    0.000 /defcon/objects/base.py:206(postNotification)
  5939280   36.517    0.000   64.005    0.000 /mutatorMath/objects/mutator.py:130(getAxisNames)
8030030/6092496   33.520    0.000   59.120    0.000 /defcon/tools/notifications.py:120(postNotification)
       36    0.007    0.000   52.835    1.468 /fontTools/ttLib/tables/otBase.py:44(compile)
 10115760   21.697    0.000   51.213    0.000 /mutatorMath/objects/location.py:353(isOnAxis)
  3742560   39.779    0.000   51.039    0.000 /mutatorMath/objects/mutator.py:278(_calcOnAxisFactor)
   300888    0.589    0.000   44.775    0.000 /defcon/objects/glyph.py:456(drawPoints)
 12467386   10.811    0.000   42.433    0.000 /booleanOperations/flatten.py:1176(_pointOnLine)
2150568/72   11.409    0.000   42.225    0.586 /fontTools/ttLib/tables/otBase.py:637(compile)

and ordered by total time:

         1120460224 function calls (1105942197 primitive calls) in 939.692 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       12  105.245    8.770  105.245    8.770 {compreffor._compreffor.compreff}
  3742560   40.403    0.000   51.647    0.000 /mutatorMath/objects/mutator.py:278(_calcOnAxisFactor)
  5939280   36.603    0.000   63.487    0.000 /mutatorMath/objects/mutator.py:130(getAxisNames)
8026338/6090928   34.563    0.000   61.369    0.000 /defcon/tools/notifications.py:120(postNotification)
 39158679   29.277    0.000   32.768    0.000 /booleanOperations/flatten.py:1172(_distance)
 10115760   22.121    0.000   51.890    0.000 /mutatorMath/objects/location.py:353(isOnAxis)
  1057680   20.320    0.000  226.982    0.000 /mutatorMath/objects/mutator.py:225(getFactors)
 10115760   19.979    0.000   24.227    0.000 /mutatorMath/objects/location.py:276(strip)
  1057680   16.710    0.000   35.874    0.000 /mutatorMath/objects/mutator.py:379(getLimits)
25856508/25856499   15.746    0.000   22.034    0.000 {built-in method builtins.hasattr}
  8081855   14.880    0.000   15.207    0.000 /booleanOperations/flatten.py:1207(_getCubicPoint)
  4881600   14.160    0.000   94.086    0.000 /mutatorMath/objects/mutator.py:248(_accumulateFactors)
  4881600   13.864    0.000   16.245    0.000 /mutatorMath/objects/location.py:295(common)
2150568/72   12.954    0.000   47.212    0.656 /fontTools/ttLib/tables/otBase.py:637(compile)
 33737479   12.419    0.000   12.419    0.000 {built-in method fromkeys}
119170426   11.906    0.000   11.906    0.000 {method 'append' of 'list' objects}
10551263/9846143   11.859    0.000   13.258    0.000 {built-in method builtins.sorted}
 12467386   11.324    0.000   43.960    0.000 /booleanOperations/flatten.py:1176(_pointOnLine)
  1057680   10.535    0.000   34.122    0.000 /mutatorMath/objects/mutator.py:139(_collectAxisPoints)
 46005507   10.026    0.000   10.026    0.000 {method 'update' of 'dict' objects}
 20573149    7.741    0.000    9.542    0.000 /defcon/objects/base.py:69(_get_dispatcher)
7934145/6094759    7.623    0.000   66.950    0.000 /defcon/objects/base.py:206(postNotification)
  1572960    7.438    0.000    8.364    0.000 /mutatorMath/objects/location.py:536(__sub__)
   175741    7.363    0.000   70.875    0.000 /booleanOperations/flatten.py:1141(_flattenSegment)
   421128    7.198    0.000    7.714    0.000 /booleanOperations/flatten.py:1118(_checkFlatPoints)
66486877/66158431    7.048    0.000    7.394    0.000 {built-in method builtins.len}
    53138    6.936    0.000    8.588    0.000 /booleanOperations/flatten.py:463(<listcomp>)
   184000    6.840    0.000   27.297    0.000 /defcon/objects/contour.py:594(drawPoints)
  6964392    6.707    0.000   11.255    0.000 /fontTools/misc/psCharStrings.py:1046(getToken)
    53138    6.616    0.000    8.736    0.000 /booleanOperations/flatten.py:654(reCurveSubSegments)
 41774229    6.521    0.000    6.521    0.000 {method 'get' of 'dict' objects}
36576931/36576930    6.237    0.000    6.305    0.000 {built-in method builtins.isinstance}
 33737280    5.988    0.000    5.988    0.000 /mutatorMath/objects/mutator.py:136(<listcomp>)
179699/108384    5.886    0.000   32.355    0.000 /fontTools/misc/psCharStrings.py:282(execute)
16333310/16081530    5.435    0.000    9.866    0.000 {built-in method builtins.getattr}
 17706822    5.311    0.000    5.311    0.000 {built-in method builtins.round}
  3377041    5.247    0.000    6.520    0.000 /defcon/objects/base.py:336(_destroyRepresentationsForNotification)
   204476    4.774    0.000    7.920    0.000 /fontMath/mathGlyph.py:495(_processMathOneContours)
    27096    4.740    0.000    4.740    0.000 {method 'Execute' of 'pyclipper.Pyclipper' objects}
 42122420    4.690    0.000    4.690    0.000 {method 'keys' of 'dict' objects}
  6446400    4.651    0.000    5.569    0.000 /fontTools/ttLib/tables/otBase.py:533(__getattr__)
  1057680    4.645    0.000    5.247    0.000 /mutatorMath/objects/mutator.py:175(_allLocations)
   421128    4.634    0.000    8.409    0.000 /booleanOperations/flatten.py:1095(<listcomp>)
3349058/931246    4.406    0.000   10.428    0.000 /usr/lib/python3.6/copy.py:132(deepcopy)
4242688/3221002    4.157    0.000   40.663    0.000 /defcon/objects/base.py:379(_set_dirty)

@khaledhosny
Copy link
Collaborator Author

Building TTF variable font ordered by cumulative time:

         227536189 function calls (220152066 primitive calls) in 184.800 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.006    0.006  185.045  185.045 /fontmake/font_project.py:380(run_from_designspace)
        1    0.000    0.000  182.374  182.374 /fontmake/font_project.py:436(run_from_ufos)
        1    0.000    0.000  118.056  118.056 /fontmake/font_project.py:195(build_interpolatable_ttfs)
        2    0.000    0.000  118.056   59.028 /fontTools/misc/loggingTools.py:369(wrapper)
        1    0.026    0.026   81.341   81.341 /fontmake/font_project.py:230(save_otfs)
        7    0.000    0.000   79.321   11.332 /ufo2ft/__init__.py:50(compileTTF)
       15    0.001    0.000   68.157    4.544 /fontTools/ttLib/__init__.py:192(save)
  261/200    0.003    0.000   68.072    0.340 /fontTools/ttLib/__init__.py:647(_writeTable)
      200    0.004    0.000   67.993    0.340 /fontTools/ttLib/__init__.py:665(getTableData)
        1    0.088    0.088   63.563   63.563 /fontmake/font_project.py:207(build_variable_font)
       25    0.010    0.000   56.120    2.245 /fontTools/ttLib/tables/otBase.py:44(compile)
        7    0.000    0.000   42.483    6.069 /ufo2ft/postProcessor.py:12(__init__)
1740839/54   10.639    0.000   42.388    0.785 /fontTools/ttLib/tables/otBase.py:637(compile)
117021/178    0.250    0.000   42.383    0.238 /fontTools/ttLib/tables/otConverters.py:509(write)
1551999/371920    1.029    0.000   39.389    0.000 /fontTools/ttLib/tables/otConverters.py:392(write)
        1    0.000    0.000   38.050   38.050 /fontTools/varLib/__init__.py:656(build)
        1    0.000    0.000   36.714   36.714 /fontmake/font_project.py:152(convert_curves)
        1    0.141    0.141   36.714   36.714 /cu2qu/ufo.py:218(fonts_to_quadratic)
       79    0.000    0.000   31.068    0.393 /fontTools/ttLib/tables/otConverters.py:550(write)
        1    0.199    0.199   24.949   24.949 /fontTools/varLib/__init__.py:240(_add_gvar)
        7    0.000    0.000   23.670    3.381 /ufo2ft/featureCompiler.py:32(compile)
 24930943   15.015    0.000   21.136    0.000 {built-in method builtins.hasattr}
     2258    0.083    0.000   20.312    0.009 /cu2qu/ufo.py:155(_glyphs_to_quadratic)
     7378    0.198    0.000   17.851    0.002 /cu2qu/ufo.py:117(_set_segments)
     7747    0.207    0.000   17.525    0.002 /fontTools/varLib/iup.py:293(iup_delta_optimize)
1649599/1634094    0.967    0.000   16.938    0.000 /defcon/objects/layer.py:259(__getitem__)
   412121    0.330    0.000   16.795    0.000 /defcon/objects/font.py:218(__getitem__)
    44272    0.278    0.000   16.456    0.000 /fontTools/varLib/iup.py:221(iup_contour_optimize)
15806/13804    0.284    0.000   15.993    0.001 /defcon/objects/layer.py:172(loadGlyph)
     8992    1.379    0.000   15.423    0.002 /fontTools/varLib/iup.py:171(_iup_contour_optimize_dp)
    66108    0.956    0.000   15.039    0.000 /ufoLib/pointPen.py:89(endPath)
  1002748    4.135    0.000   13.968    0.000 /fontTools/varLib/iup.py:94(can_iup_in_between)
    66108    1.239    0.000   13.915    0.000 /ufoLib/pointPen.py:151(_flushContour)
       54    0.136    0.003   13.715    0.254 /fontTools/ttLib/tables/otBase.py:374(getAllData)
  2894093    2.804    0.000   13.122    0.000 /fontTools/ttLib/tables/otConverters.py:580(write)
        1    0.000    0.000   12.767   12.767 /fontTools/varLib/__init__.py:482(_merge_OTL)
        1    0.000    0.000   12.718   12.718 /fontTools/varLib/merger.py:103(mergeTables)
15806/13804    0.076    0.000   12.702    0.001 /ufoLib/glifLib.py:266(readGlyph)
        7    0.000    0.000   12.290    1.756 /ufo2ft/featureCompiler.py:126(setupFile_featureTables)
        7    0.046    0.007   12.286    1.755 /fontTools/feaLib/builder.py:19(addOpenTypeFeaturesFromString)
        7    0.000    0.000   12.240    1.749 /fontTools/feaLib/builder.py:14(addOpenTypeFeatures)
        7    0.001    0.000   12.240    1.749 /fontTools/feaLib/builder.py:77(build)
        7    0.008    0.001   11.377    1.625 /ufo2ft/featureCompiler.py:43(setupFile_features)
15806/13804    0.086    0.000   11.192    0.001 /ufoLib/glifLib.py:823(_readGlyphFromTree)
15806/13804    0.220    0.000   11.106    0.001 /ufoLib/glifLib.py:890(_readGlyphFromTreeFormat2)

and ordered by total time:

         227549414 function calls (220163206 primitive calls) in 153.634 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
 24930943   13.429    0.000   18.431    0.000 {built-in method builtins.hasattr}
1740839/54    9.799    0.000   38.805    0.719 /fontTools/ttLib/tables/otBase.py:637(compile)
  1002748    5.511    0.000    6.497    0.000 /fontTools/varLib/iup.py:6(iup_segment)
1237210/991920    3.894    0.000    5.791    0.000 /defcon/tools/notifications.py:120(postNotification)
  5574795    3.878    0.000    4.614    0.000 /fontTools/ttLib/tables/otBase.py:533(__getattr__)
  1002748    3.538    0.000   12.008    0.000 /fontTools/varLib/iup.py:94(can_iup_in_between)
  2894093    2.644    0.000   11.491    0.000 /fontTools/ttLib/tables/otConverters.py:580(write)
   507584    2.434    0.000    3.985    0.000 /fontTools/feaLib/lexer.py:61(next_)
 26240833    2.243    0.000    2.243    0.000 {method 'append' of 'list' objects}
    93099    2.215    0.000    2.268    0.000 {built-in method builtins.eval}
 13737107    2.133    0.000    2.241    0.000 {method 'get' of 'dict' objects}
  2894093    2.091    0.000    8.227    0.000 /fontTools/ttLib/tables/otBase.py:870(writeValueRecord)
    44464    1.694    0.000    6.699    0.000 /defcon/objects/contour.py:594(drawPoints)
4774526/4773639    1.673    0.000    1.673    0.000 {built-in method builtins.getattr}
2909625/2366043    1.609    0.000    2.411    0.000 /fontTools/ttLib/tables/_g_l_y_f.py:1206(__getitem__)
 356509/9    1.397    0.000    6.844    0.760 /fontTools/ttLib/tables/otBase.py:597(decompile)
  8962537    1.367    0.000    1.413    0.000 {built-in method builtins.isinstance}
188840/54    1.340    0.000    4.418    0.082 /fontTools/ttLib/tables/otBase.py:288(_doneWriting)
  2140512    1.290    0.000    1.481    0.000 /fontTools/varLib/iup.py:101(<genexpr>)
   624862    1.257    0.000    2.553    0.000 /fontTools/ttLib/tables/otBase.py:849(readValueRecord)
  3253429    1.253    0.000    1.559    0.000 /defcon/objects/base.py:69(_get_dispatcher)
   845637    1.201    0.000    1.201    0.000 /fontTools/feaLib/lexer.py:161(scan_over_)
     8992    1.189    0.000   13.260    0.001 /fontTools/varLib/iup.py:171(_iup_contour_optimize_dp)
  3913908    1.182    0.000    1.182    0.000 {built-in method _struct.pack}
  1569555    1.167    0.000    1.859    0.000 /fontTools/ttLib/tables/otBase.py:420(writeShort)
   144543    1.140    0.000    3.331    0.000 /fontTools/ttLib/tables/otBase.py:241(getDataLength)
1233211/1009558    1.127    0.000    6.691    0.000 /defcon/objects/base.py:206(postNotification)
     2870    1.094    0.000    1.099    0.000 /ufo2ft/kernFeatureWriter.py:370(_getGlyphKerning)
10238717/10012844    1.070    0.000    1.169    0.000 {built-in method builtins.len}
144543/54    1.040    0.000    2.267    0.042 /fontTools/ttLib/tables/otBase.py:318(_gatherTables)
   148419    1.022    0.000    4.172    0.000 /fontTools/ttLib/tables/_g_l_y_f.py:1235(extend)
        7    1.021    0.146    1.021    0.146 {built-in method posix.waitpid}
1551999/371920    1.011    0.000   35.722    0.000 /fontTools/ttLib/tables/otConverters.py:392(write)
602233/601926    0.981    0.000    2.158    0.000 {built-in method builtins.setattr}
    10822    0.902    0.000    1.775    0.000 /ufoLib/glifLib.py:1178(_validateAndMassagePointStructures)
  1531762    0.892    0.000    2.538    0.000 /fontTools/ttLib/tables/_g_l_y_f.py:1185(_checkFloat)
    66108    0.874    0.000   10.067    0.000 /ufoLib/pointPen.py:151(_flushContour)
  1188234    0.801    0.000    1.872    0.000 /defcon/objects/font.py:214(__iter__)
  1104974    0.761    0.000    1.242    0.000 /fontTools/ttLib/tables/otBase.py:416(writeUShort)
   315162    0.736    0.000    2.033    0.000 /ufo2ft/kernFeatureWriter.py:443(_glyphIsRtl)
1649599/1634094    0.724    0.000   10.486    0.000 /defcon/objects/layer.py:259(__getitem__)
   341765    0.718    0.000    0.718    0.000 /ufoLib/glifLib.py:1279(_number)
  1358578    0.704    0.000    2.940    0.000 {built-in method builtins.all}
  1740845    0.702    0.000    0.986    0.000 /fontTools/ttLib/tables/otBase.py:544(ensureDecompiled)
   757209    0.691    0.000    0.965    0.000 /fontTools/ttLib/__init__.py:593(getGlyphID)

@khaledhosny
Copy link
Collaborator Author

With PyPy it takes ~6 minutes to build everything, but without overlap removal and probably CFF optimization:

real	5m55.376s
user	5m50.792s
sys	0m5.334s

Python 3:

real	9m44.509s
user	9m39.684s
sys	0m4.949s

@chrissimpkins
Copy link
Member

but without overlap removal and probably CFF optimization

@khaledhosny is it necessary to modify the logging level to see that is the case during the build? The default standard output looks the same for me (i.e. no notification of error with any of the steps in the build process) as it does for our cPython builds (PyPy stdout shown):

INFO:fontmake.font_project:Building OTFs
INFO:fontmake.font_project:Decomposing glyphs for Hack-Regular
INFO:fontmake.font_project:Removing overlaps for Hack-Regular
INFO:fontmake.font_project:Saving OTF for Hack-Regular
INFO:fontmake.font_project:Building TTFs
INFO:fontmake.font_project:Decomposing glyphs for Hack-Regular
INFO:fontmake.font_project:Removing overlaps for Hack-Regular
INFO:fontmake.font_project:Converting curves for Hack-Regular
INFO:cu2qu.ufo:New spline lengths: 1: 4908, 2: 4107, 3: 504, 4: 71, 5: 16
INFO:fontmake.font_project:Saving TTF for Hack-Regular
INFO:fontmake.font_project:Building OTFs
INFO:fontmake.font_project:Decomposing glyphs for Hack-Bold
INFO:fontmake.font_project:Removing overlaps for Hack-Bold
INFO:fontmake.font_project:Saving OTF for Hack-Bold
INFO:fontmake.font_project:Building TTFs
INFO:fontmake.font_project:Decomposing glyphs for Hack-Bold
INFO:fontmake.font_project:Removing overlaps for Hack-Bold
INFO:fontmake.font_project:Converting curves for Hack-Bold
INFO:cu2qu.ufo:New spline lengths: 1: 4723, 2: 4510, 3: 448, 4: 69, 5: 17, 6: 4, 7: 3
INFO:fontmake.font_project:Saving TTF for Hack-Bold
INFO:fontmake.font_project:Building OTFs
INFO:fontmake.font_project:Decomposing glyphs for Hack-Italic
INFO:fontmake.font_project:Removing overlaps for Hack-Italic
INFO:fontmake.font_project:Saving OTF for Hack-Italic
INFO:fontmake.font_project:Building TTFs
INFO:fontmake.font_project:Decomposing glyphs for Hack-Italic
INFO:fontmake.font_project:Removing overlaps for Hack-Italic
INFO:fontmake.font_project:Converting curves for Hack-Italic
INFO:cu2qu.ufo:New spline lengths: 1: 5144, 2: 5262, 3: 524, 4: 80, 5: 12, 6: 1
INFO:fontmake.font_project:Saving TTF for Hack-Italic
INFO:fontmake.font_project:Building OTFs
INFO:fontmake.font_project:Decomposing glyphs for Hack-BoldItalic
INFO:fontmake.font_project:Removing overlaps for Hack-BoldItalic
INFO:fontmake.font_project:Saving OTF for Hack-BoldItalic
INFO:fontmake.font_project:Building TTFs
INFO:fontmake.font_project:Decomposing glyphs for Hack-BoldItalic
INFO:fontmake.font_project:Removing overlaps for Hack-BoldItalic
INFO:fontmake.font_project:Converting curves for Hack-BoldItalic
INFO:cu2qu.ufo:New spline lengths: 1: 4971, 2: 5395, 3: 455, 4: 85, 5: 7, 6: 1
INFO:fontmake.font_project:Saving TTF for Hack-BoldItalic
real        67.83
user        66.29
sys          1.35

@khaledhosny
Copy link
Collaborator Author

Sorry my wording was misleading, I intentionally disabled overlap removal and CFF optimization to speed non-release builds (I was saying something else above originally, but edited the text before posting).

@anthrotype
Copy link
Member

There are specific options for keeping overlaps and disable subroutinization. Those have nothing to do with logging

@chrissimpkins
Copy link
Member
chrissimpkins commented Nov 4, 2017

Ah, I see. Misunderstood. Thought that you meant that (at least a portion of) the decrease in time was due to skipped routines.

@anthrotype
Copy link
Member

Thanks both for the profiler data you have collected, it's very helpful.
I prefer to defer this to when I'll have time to properly tackle this, as it requires some rethinking of the fontmake interface and also of some of its dependency.
For now I'd recommend you parallelize with GNU make or using your ad-hoc python scripts, or use PyPy which gets you 30% faster for free.
I don't think it's fontmake's responsibility to document how to do the above.

@khaledhosny
Copy link
Collaborator Author

It would be helpful, though, if there was a way to generate a single UFO instance from a designspace file, right now all instances are generated once and this makes it hard to do things in parallel (more so with make, as one rule making multiple targets is not compatible with parallelism).

@anthrotype
Copy link
Member

I know... But I believe mutatorMath API doesn't allow that. At least mutatorMath.ufo.build (which is what fontmake is using) always instantiates all the instances in the designspace.

@behdad
Copy link
Contributor
behdad commented Nov 4, 2017

Nice discussion, I have had missed this.

So, this is the first time I'm seeing PyPy perform better than cPython. Good to know.

Looks like majority of time is spent in booleanOperations.flatten; so we're back to replacing that. Let's figure out who's going to work on that. cc @adrientetar

@anthrotype
Copy link
Member

the majority of time for compiling a single UFO is spent on booleanOperations, yes. But when compiling a designspace, the majority of time is spent in mutatorMath/fontMath/defcon/...

@behdad
Copy link
Contributor
behdad commented Nov 4, 2017

For starters, how about changing epsilon in booleanOperations flatten.py from 1e-8 to 1e-4? @typemytype

@behdad
Copy link
Contributor
behdad commented Nov 4, 2017

And for mutatorMath/fontMath/defcon, we should add a lean ufoLib to fontTools, with minimal Glyph and Font and Contour etc objects that already do math, and use varLib on them.

@anthrotype
Copy link
Member
anthrotype commented Nov 4, 2017

this is the first time I'm seeing PyPy perform better than cPython

I hear pypy works best with long running processes as its JIT compiler takes some time to heat up

@chrissimpkins
Copy link
Member
chrissimpkins commented Nov 4, 2017

best with long running processes as its JIT compiler takes some time to heat up

@anthrotype below is from PyPy web site:

There are two cases that you should be aware where PyPy will not be able to speed up your code:

  • Short-running processes: if it doesn't run for at least a few seconds, then the JIT compiler won't have enough time to warm up.
  • If all the time is spent in run-time libraries (i.e. in C functions), and not actually running Python code, the JIT compiler will not help.

So the case where PyPy works best is when executing long-running programs where a significant fraction of the time is spent executing Python code. This is the case covered by the majority of our benchmarks, but not all of them – the goal of PyPy is to get speed but still support (ideally) any Python program.

@behdad
Copy link
Contributor
behdad commented Nov 5, 2017

For starters, how about changing epsilon in booleanOperations flatten.py from 1e-8 to 1e-4? @typemytype

With this change, time fontmake -g NotoSansArabic-MM.glyphs -o ttf goes down from 2min to... 2min!

1 0.000 0.000 16.513 16.513 font_project.py:431(run_from_ufos)
1 0.000 0.000 16.476 16.476 font_project.py:172(build_ttfs)
4 0.000 0.000 16.475 4.119 loggingTools.py:369(wrapper)
1 0.017 0.017 11.306 11.306 font_project.py:96(remove_overlaps)
1567 0.051 0.000 10.227 0.007 booleanOperationManager.py:102(union)
1567 0.052 0.000 10.176 0.006 booleanOperationManager.py:62(_performOperation)
1567 0.006 0.000 7.465 0.005 booleanOperationManager.py:64()
2878 0.024 0.000 7.454 0.003 flatten.py:65(init)
5756 0.075 0.000 7.075 0.001 flatten.py:421(_convertPointsToSegments)
51828 0.148 0.000 6.991 0.000 flatten.py:142(init)
9606 0.493 0.000 5.256 0.001 flatten.py:1141(_flattenSegment)

I cannot reproduce this. flatten.py takes, like, 10 seconds out of 180s runtime on NotoSansArabic-MM.glyphs for me.

@chrissimpkins
Copy link
Member
chrissimpkins commented Nov 5, 2017

@behdad If you'd like to replicate with the source that I used:

Data that I showed indicating the issue with flatten.py in:

were direct builds from UFO source files to ttf / otf binaries using FontProject.run_from_ufos() with the regular variant of the UFO source directories located here:

https://github.com/source-foundry/Hack/tree/master/source

tar.gz download: https://github.com/source-foundry/Hack/archive/v3.000.tar.gz

The parallel build data that I posted above included all four variant UFO source directories in the repository link above.

I don't believe anyone has presented profiling directly from glyphs source files yet. Not sure how that might influence the booleanOperations issue that occurred with my testing.

@typemytype
Copy link

For starters, how about changing epsilon in booleanOperations flatten.py from 1e-8 to 1e-4? @typemytype

dont know if that is going to change that much, BooleanOperations is and always will be a heavy and time consuming call...

I guess the best option is to port to a more completely non py package... as proposed here typemytype/booleanOperations#40

@chrissimpkins
Copy link
Member

Pushed this for anyone who is building directly from UFO source and might find it of use. It automates the number of spawned processes for requested compiles up to a max of the number of cores detected on the system (can be manually set if desired):

fmp

Builds ttf + otf by default. Modify directly in the script. Details on README.

@behdad
Copy link
Contributor
behdad commented Nov 9, 2017

Thanks! We should add a mode to fontmake to do this automatically.

@chrissimpkins
Copy link
Member

Thanks! We should add a mode to fontmake to do this automatically.

Agree! It sounds like @anthrotype plans to tackle this at some stage. #367 (comment)

@davelab6
Copy link
Member
davelab6 commented Feb 2, 2019 via email

@khaledhosny
Copy link
Collaborator Author

I don’t know, but it does not really matter. Python is slow and I don’t think this is going to change.

@khaledhosny
Copy link
Collaborator Author

FWIW, I manged to cut the build time by half (down to 15 minutes from 30 minutes) by using --optimize-cff=0 and --keep-overlaps, this produces bigger files and overlapping glyphs, but meh.

I managed to cut down to 4 minutes by building only the masters with fontmake, using varLib to build a variable font out of them, and varLib.mutator to create the instances from it.

@madig
Copy link
Collaborator
madig commented May 18, 2019

@khaledhosny If you're bored, try building Mada instances with https://gitlab.gnome.org/GNOME/cantarell-fonts/blob/master/scripts/make-static-fonts.py and https://gitlab.gnome.org/GNOME/cantarell-fonts/blob/master/scripts/instantiator.py. It launches a subprocess per instance and psautohints the resulting OTF. I can build the 14 Noto Sans uprights OTFs in 2:50 mins (without autohinting) on my 16 logical cores, only takes around 7 GB of RAM for the duration.

@typoman
Copy link
typoman commented May 18, 2019

Not sure if this helps but the command checkoutlinesufo -e from FDK adds another layer named glyphs.com.adobe.type.processedglyphs to UFO which contains glyphs that have their overlaps removed. It uses a hash map to make sure if the glyph has changed or not. This fastens the build process and I think this could be useful to make ufo2ft faster.

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

No branches or pull requests

9 participants
0