Last week I investigated the problem of too slow Krita's build speed on the Windows CI and I found quite a few interesting issues that I would like to discuss with the wider community.
Problem definition
- Our libraries export too many include paths in their public interface E.g. kritaui has 97(!) inclide path, which are passed with -I option. You can see them in the list below.
Most of these paths are basically useless, since just link to the build folders of the various libraries (for foobar_export.h headers and configuration files). The other part of the paths is the subfolders of the library, like 9(!) subfolders of kritaui and 5(!) subfolders of kritaimage.
-IC:/dev/env-8/b-krita-ccache/libs/ui -IC:/dev/env-8/krita/libs/ui -IC:/dev/env-8/b-krita-ccache/libs/ui/kritaui_autogen/include -IC:/dev/env-8/b-krita-ccache -IC:/dev/env-8/krita -IC:/dev/env-8/krita/winquirks -IC:/dev/env-8/krita/libs/ui/qtlockedfile -IC:/dev/env-8/krita/libs/ui/canvas -IC:/dev/env-8/krita/libs/ui/flake -IC:/dev/env-8/krita/libs/ui/ora -IC:/dev/env-8/krita/libs/ui/tool -IC:/dev/env-8/krita/libs/ui/utils -IC:/dev/env-8/krita/libs/ui/widgets -IC:/dev/env-8/krita/libs/ui/widgets/gradient -IC:/dev/env-8/krita/libs/ui/input/wintab -IC:/dev/env-8/b-krita-ccache/libs/version -IC:/dev/env-8/krita/libs/version -IC:/dev/env-8/b-krita-ccache/libs/impex -IC:/dev/env-8/krita/libs/impex -IC:/dev/env-8/krita/libs/image/brushengine -IC:/dev/env-8/krita/libs/image/filter -IC:/dev/env-8/krita/libs/image/generator -IC:/dev/env-8/krita/libs/image/layerstyles -IC:/dev/env-8/krita/libs/image/processing -IC:/dev/env-8/b-krita-ccache/libs/image -IC:/dev/env-8/krita/libs/image -IC:/dev/env-8/b-krita-ccache/libs/widgets -IC:/dev/env-8/krita/libs/widgets -IC:/dev/env-8/b-krita-ccache/libs/global -IC:/dev/env-8/krita/libs/global -IC:/dev/env-8/krita/libs/flake/commands -IC:/dev/env-8/krita/libs/flake/tools -IC:/dev/env-8/krita/libs/flake/svg -IC:/dev/env-8/krita/libs/flake/text -IC:/dev/env-8/b-krita-ccache/libs/flake -IC:/dev/env-8/krita/libs/flake -IC:/dev/env-8/krita/libs/pigment/resources -IC:/dev/env-8/krita/libs/pigment/compositeops -IC:/dev/env-8/b-krita-ccache/libs/pigment -IC:/dev/env-8/krita/libs/pigment -IC:/dev/env-8/b-krita-ccache/libs/koplugin -IC:/dev/env-8/krita/libs/koplugin -IC:/dev/env-8/b-krita-ccache/libs/store -IC:/dev/env-8/krita/libs/store -IC:/dev/env-8/b-krita-ccache/libs/resources -IC:/dev/env-8/krita/libs/resources -IC:/dev/env-8/b-krita-ccache/libs/command -IC:/dev/env-8/krita/libs/command -IC:/dev/env-8/krita/libs/widgetutils/config -IC:/dev/env-8/krita/libs/widgetutils/xmlgui -IC:/dev/env-8/b-krita-ccache/libs/widgetutils -IC:/dev/env-8/krita/libs/widgetutils -IC:/dev/env-8/b-krita-ccache/libs/multiarch -IC:/dev/env-8/krita/libs/multiarch -IC:/dev/env-8/b-krita-ccache/libs/resourcewidgets -IC:/dev/env-8/krita/libs/resourcewidgets -IC:/dev/env-8/b-krita-ccache/libs/psdutils -IC:/dev/env-8/krita/libs/psdutils -IC:/dev/env-8/b-krita-ccache/libs/metadata -IC:/dev/env-8/krita/libs/metadata -IC:/dev/env-8/b-krita-ccache/libs/color -IC:/dev/env-8/krita/libs/color -IC:/dev/env-8/b-krita-ccache/libs/brush -IC:/dev/env-8/krita/libs/brush -Isystem C:/dev/env-8/i/include/QtGui/5.15.7 -Isystem C:/dev/env-8/i/include/QtGui/5.15.7/QtGui -Isystem C:/dev/env-8/i/include/QtCore/5.15.7 -Isystem C:/dev/env-8/i/include/QtCore/5.15.7/QtCore -Isystem C:/dev/env-8/i/include/KF5/KCoreAddons -Isystem C:/dev/env-8/i/include/KF5 -Isystem C:/dev/env-8/i/include -Isystem C:/dev/env-8/i/include/QtCore -Isystem C:/dev/env-8/i/./mkspecs/win32-clang-g++ -Isystem C:/dev/env-8/i/include/KF5/KCompletion -Isystem C:/dev/env-8/i/include/QtWidgets -Isystem C:/dev/env-8/i/include/QtGui -Isystem C:/dev/env-8/i/include/KF5/KI18n -Isystem C:/dev/env-8/i/include/KF5/KItemViews -Isystem C:/dev/env-8/i/include/QtNetwork -Isystem C:/dev/env-8/i/include/eigen3 -Isystem C:/dev/env-8/i/include/boost-1_80 -Isystem C:/dev/env-8/i/include/QtConcurrent -Isystem C:/dev/env-8/i/include/QtXml -Isystem C:/dev/env-8/i/include/QtSql -Isystem C:/dev/env-8/i/include/KF5/KConfig -Isystem C:/dev/env-8/i/include/KF5/KConfigGui -Isystem C:/dev/env-8/i/include/KF5/KConfigCore -Isystem C:/dev/env-8/i/include/OpenEXR -Isystem C:/dev/env-8/i/include/KF5/KWidgetsAddons -Isystem C:/dev/env-8/i/include/QtSvg -Isystem C:/dev/env-8/i/include/freetype2 -Isystem C:/dev/env-8/i/include/harfbuzz -Isystem C:/dev/env-8/i/include/QtPrintSupport -Isystem C:/dev/env-8/i/include/KF5/KGuiAddons -Isystem C:/dev/env-8/i/include/SDL2 -Isystem C:/dev/env-8/i/include/mlt-7 -Isystem C:/dev/env-8/i/include/mlt-7/mlt++
- Every time clang-15.exe or moc.exe see #include <foobar.h> line in the source it has to browse through the entire list to locate the actual location of the header.
- Clang and moc use GetFileAttributes and GetFileAttributesEx API calls correspondingly to check if the file exists in a particular location. And the problem is that these calls are EXTREMELY SLOW on Windows running over NTFS filesystem. And, yes, that is the fastest way available on Windows for checking for the existence of the file, and, yes, NTFS is the only filesystem available on Windows :(
I personally tried to use CreateFile method for moc.exe, but it was much slower.
- GetFileAttributesEx is so slow that fully(!) ccache'd build of Krita on CI takes about 25 minutes(!) and all this time is spent on generation of the .moc files. And 78% of the time in moc.exe is spent in GetFileAttributesEx, looking up header files...
- Some of the include paths (e.g. freetype2 and harfbuzz) appear in the list because the library exports them to PUBLIC for unittests.
So we have a problem of extremely slow builds on Windows (and Windows CI). And the cause of these slow builds is a mess in includes that our libraries export (via target_link_libraries(foo PUBLIC bar)). I did some experiments with that, so I would like to propose a set of guidelines/policies on how we should manage our inter-krita library dependencies:
Proposed guidelines (to be discussed)
I made a list of guidelines that could probably help reduce the amount of libraries we link. I don't feel very confident in this topic, so I would really like to hear some input on that:
- KDECMakeSettings scripts enables CMAKE_INCLUDE_CURRENT_DIR and CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE for the entire Krita project. These macros add current source and build directories into the public interface of the library. We should disable that and add current directory only into PRIVATE section and only when needed.
- TODO: how should we do that? patch KDECMakeSettings or just unset variable in our code right after the include? (the latter approach works in the current version of CMake, I tested, but it is basically UB)
- All configure_file and generate_export_header macros should generate the headers into some common directory, e.g. <krita-build-root>/generated. Then we won't have a 21 include paths, each containing a single generated header.
- A krita library (e.g. kritaimage) should publish only one "root" library. E.g. for kritaimage it is <krita-src-root>/libs/image
- [NEEDS DISCUSSION] Subfolders. Subfolders is a big problem. I'm not really sure how to handle it. My original suggestion was like that:
- if files from a subfolder are exported a lot, then just cut it off it into a separate library (e.g. libs/widgetutils/xmlgui)
- if files from a subfolder are mostly used inside the library, then keep them in a subfolder and access via angled notation with a subfolder name, e.g. #include <layerstyles/kis_layer_style_projection_plane.h>
TODO: The main problem of this approach is that I'm not sure that splitting stuff into a separate library will recude the number of -I switches in the kritaui library. Perhaps we have an issue that kritaui has become a "superlibrary" that links to everything?
- [NEEDS DISCUSSION] As far as I can understand we should prefer angular includes for the files that might be used outside of the current library, e.g. #include <kis_image.h> instead of `#include "kis_image.h". That is google's policy for includes, and, afaict, the reasoning for that is that the meaning for ""-includes is implementation-defined and differs between MSVC and gcc/clang.
- We shouldn't export a library via PUBLIC interface if the only use for this export is a unittest. It pollutes the overal interface of the library.
- TODO: should we introduce some custom properties like UNITTEST_INTERFACE_LIBRARIES for that?