Tuesday, May 09, 2023

Tips for making MySQL builds & tests faster

Recently, Mark discovered that a part of FB MySQL sources were recompiled three times in a single build. That has been fixed, and I also played with clang -ftime-trace to see where the build time goes. I believe there is more to this topic, so I wanted to organize my thoughts on the subject.

In software development, the shorter the change-build-test cycle is, the higher the developer productivity. Yet MySQL is not making it easy to have short "build" and "test" steps in this cycle. A reasonably powerful Intel Core i9 laptop used to take 20-30 minutes for a clean debug build without the unit tests. The MTR testsuite takes hours, and it is possible to make it run for days if you wish.

What can be done about this? Here's what's working for me, focusing on 8.0 trees. There is no silver bullet here, and some of the suggestions might be even somewhat effort-intensive to set up. Luckily, most suggestions are independent and optional.

Source trees and build artifacts

TL;DR: build incrementally as much as possible. Disk space is cheap, while your time is expensive. A common "antipattern" (quotes because there is nothing wrong with such workflow otherwise) is to have a single local git clone with a single build directory. Changing a branch with git checkout forces a clean build. Changing the build type (i.e. you built Debug previously, now need RelWithDebInfo, or Debug + AddressSanitizer enabled) forces a clean build. Don't force clean builds; keep all the previously-built artifacts around as much as possible:

  • Have one build dir for every build type (e.g. build/debug, build/release, build/debug-asan). Never delete a build dir unless forced to.
  • Never use git checkout in the local clone directory, use git worktrees for everything. Only delete a worktree (and its build dirs) once that branch is merged.
  • You are free to store the build dirs wherever you want, but in order to keep track which build dir belongs to which source tree, for me, the simplest option was to keep them below the source tree. Oracle MySQL has build/ in its .gitignore, so that's a good prefix dir for them.

One objection to incremental builds is that they might somehow result in differences in build artifacts compared to the same clean builds, and that would be bad. In practice, sometimes something does break an incremental build with a build error, forcing a clean rebuild–an occasional checkpoint if you will. I have never encountered a silent divergence that was somehow detrimental, and your CI/CD farm will build your PRs cleanly anyway.

Now if you follow the worktree advice, you are likely to have quite a few of them. Some of them will be your personal feature branches, while others will be shared feature branches, and main/master/8.0/5.7 trunk branches where others commit and push. Set up a cron job to pull the later and build overnight. Pros: ready builds for your work in the morning. Cons: you left your work last night with a working build, and someone pushed a commit that broke the build for you, which is what you find in the morning. IMHO the pros outweigh the cons.

Some MySQL source trees, like the Meta one, are incompatible with git worktrees. Luckily there is a not too-complicated workaround of adding the following CMake options: -DMYSQL_GITHASH=0 -DMYSQL_GITDATE=2100-02-29 -DROCKSDB_GITHASH=0 -DROCKSDB_GITDATE=2100-02-29. Maybe one day it will be fixed properly.

A thing that did not work well for me is ccache. While easy to set up, at least twice I had to waste a lot of time on apparent source-binary mismatch only to figure out that ccache is substituting an incorrect binary object file. That was enough for me to drop it, and I could never measure its benefit anyway.

Build options

According to the MySQL docs, there are over 160 CMake options. Some of them can affect the build times for better.

Use system libraries as much possible

You are in the business of developing MySQL, not its bundled 3rd party libraries. If you are lucky, you are also not in the business of developing MySQL integration with any of them. So, ignore the bundled libraries as much as possible and use your system ones: -DWITH_SYSTEM_LIBS=ON, after installing all the dependencies (which I won't list here). Unfortunately, that's only a theory, and in practice there's a difference between theory and practice. Let's take macOS, for example, to see which of the bundled still have to be used:

  • 8.0.33: -DWITH_RAPIDJSON=bundled
  • 8.0.32-29: system libs only, yay!
  • 8.0.28-27: -DWITH_RAPIDJSON=bundled -DWITH_LZ4=bundled -DWITH_FIDO=bundled
  • 8.0.26: -DWITH_RAPIDJSON=bundled -DWITH_LZ4=bundled

That's not too bad, and, given that we are stuck with every release for at least three months (much longer than that if using, say, the Meta tree), it's worth figuring out.

Skip the unit tests but be careful

-DWITH_UNIT_TESTS=OFF is by far the single most time-saving CMake option. Usually it is also not as bad as it may sound for development because the MTR tests are still there, and depending on what you are working on, the MTR tests might cover your testing needs completely. The biggest risk there is updating some internal API in a not particularly interesting way and forgetting to update its users in the unit tests. I am still figuring out the best way here to have my cake and eat it too.

Older versions: skip the functionality you don't need

This is quickly becoming an obsolete tip, but including it for completeness. Group replication used to be an optional build part, and the X plugin still is (-DWITH_MYSQLX=OFF). Unfortunately, disabling the X plugin breaks quite a few unrelated-to-X MTR tests, so I don't do that anymore.

Testing

Use libeatmydata

Install Stewart Smith's libeatmydata and always use it, except if building with sanitizers on Linux. It is packaged for Ubuntu, macOS Homebrew, and likely elsewhere. It cuts about 25% of MTR testsuite runtime by silently substituting all the fsync and related calls with no-ops for the tested processes. It is transparent, invisible, not getting in your way etc.–a pure win. Its invocation in the context of MTR tests is a handful to type, so I am using a shell script helper:


UNAME_OUT="$(uname -s)"
if [ "$UNAME_OUT" = "Darwin" ]; then
    if [ "$(arch)" = "arm64" ]; then
        BREW="/opt/homebrew/opt"
    else
        BREW="/usr/local/opt"
    fi
    EMD_LIBDIR="$BREW/libeatmydata/lib"
    unset BREW
    export MTR_EMD=(
        "–mysqld-env=DYLD_LIBRARY_PATH=$EMD_LIBDIR"
        "–mysqld-env=DYLD_FORCE_FLAT_NAMESPACE=1"
        "–mysqld-env=DYLD_INSERT_LIBRARIES=$EMD_LIBDIR/libeatmydata.dylib")
    unset EMD_LIBDIR
else
    export MTR_EMD=(
        "–mysqld-env=LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libeatmydata.so")
fi

mtr_emd() {
    ./mtr "${MTR_EMD[@]}" "$@"
}

MTR also provides the --mem option which tries to use a non-persistent filesystem for running tests. In theory this should speed it up even more than libeatmydata, but I could never get it work reliably.

Use --parallel, find its best value for your machine & your tests.

For instance, for single-server tests on an Apple M1 Max (8 performance and 2 efficiency cores) the fastest I found was --parallel=15. Replication tests complicate this by spawning three servers per test instead of one.

Don't test what you don't need

Now we are deep in Captain Obvious territory, but still. Developing a plugin, say clone? Great, most of the time --suite=clone is enough. Sure, even with plugins the separation is not perfect, and there are dependencies, for example group replication uses clone too, but it's a start. You are less lucky if you work on InnoDB, or something in i.e. THD or Handler that is a dependency of everything.

Hardware

Throwing hardware at the build time problem is a great option if you have the resources. IMHO, there are two main routes to consider: the offline-portable one & the connected-powerful one.

If you need a laptop and want the option of working offline, then Apple Silicon is second to none. Replacing an Intel Core i9 laptop with a M1 Max one made MySQL builds five times faster. Five actual times! The downside is that macOS is not exactly a datacenter server OS, and so if you are the only one on the team on macOS, guess who just became the macOS port maintainer? There is an option of running Linux on Apple Silicon, which I heard virtualizes at near-bare metal speed, but I haven't explored it yet. Even then you become the ARM port maintainer, which is not that bad considering Graviton.

If a desktop is OK, then there are options, although I haven't tried this myself. Apple should still be in the running, and you could get an AMD CPU with up to 64 cores, which should make a short work of even MySQL build.

If a network is OK, then Sunny recommends using icecream to distribute compilation. I haven't tried this either.

Conclusion

I wrote down everything I knew about making MySQL build and test faster. Have I missed anything? Please comment.

Thursday, April 27, 2023

MySQL, clang -ftime-trace, & ClangBuildAnalyzer

TL;DR: install ClangBuildAnalyzer, compile MySQL with CC='clang -ftime-trace' CXX='clang++ -ftime-trace', run the analyzer, enjoy nice reports telling you why it's so slow.

Inspired by Mark's question on Twitter, I decided to try out clang -ftime-trace on MySQL source tree. This clang flag was added by Aras Pranckevičius (sure I'll call out a Lithuanian author by name) to LLVM 9.0, and it outputs per-source-file JSONs in Chrome Tracing format explaining where did the compilation time go. Now, instead of stating that for this file parsing took X seconds, template instantiation Y minutes, and register allocation Z microseconds it provides actionable information with object granularity, i.e. it tells you which exactly templates were expensive to instantiate etc.

To get those reports, -ftime-trace needs to be added to compilation flags. Don't add it to MySQL CMake CMAKE_CXX_FLAGS etc, because you'll have to get the rest of -O2 -DNDEBUG etc. right. Instead, pretend that this flag is a part of compiler invocation itself, and set CMAKE_CXX_COMPILER='clang++ -ftime-trace', and likewise for CMAKE_C_COMPILER. Or, use CC and CXX environment variables as I did in the TL;DR section.

A clean MySQL 8.0.33 results in a few thousands of those JSONs. Each one could be loaded in Chrome by navigating to chrome://tracing and loading a file. Individually. This blog post suggests zipping them all up, enabling Chrome to load that zip instead. But the post also warns that it takes a long time to load, and I can confirm Chrome crashing after some 40 minutes of work.

Luckily for us Aras wrote a project-level analyzer tool: ClangBuildAnalyzer. It handles those thousands of JSONs just fine, and very quickly, and for a regular 8.0.33 Release configuration build outputs the following:


**** Time summary:
Compilation (6522 times):
  Parsing (frontend):         4669.4 s
  Codegen & opts (backend):   3139.1 s

**** Files that took longest to parse (compiler frontend):
 16499 ms: ./unittest/gunit/xplugin/xpl/CMakeFiles/xpl_test_src.dir/mock/mock.cc.o
 12057 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/session_execute_t.cc.o
 12011 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/session_connect_t.cc.o
 11907 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/session_options_t.cc.o
 11533 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/session_negotiation_t.cc.o
 11454 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/protocol_send_recv_t.cc.o
 11085 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/session_capability_t.cc.o
 10930 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/auth_chaining_t.cc.o
 10201 ms: ./unittest/gunit/xplugin/xpl/CMakeFiles/xpl_test_src.dir/timeouts_t.cc.o
 10195 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/session_general_t.cc.o

**** Files that took longest to codegen (compiler backend):
 48974 ms: ./unittest/gunit/innodb/CMakeFiles/merge_innodb_tests-t.dir/ut0new-t.cc.o
 31598 ms: ./unittest/gunit/xplugin/xpl/CMakeFiles/xpl_test_src.dir/mock/mock.cc.o
 30849 ms: ./sql/CMakeFiles/sql_gis.dir/gis/intersection_functor.cc.o
 30007 ms: ./sql/CMakeFiles/sql_gis.dir/gis/difference_functor.cc.o
 29611 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/auth_chaining_t.cc.o
 27047 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/session_connect_t.cc.o
 26776 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/session_negotiation_t.cc.o
 26253 ms: ./unittest/gunit/CMakeFiles/merge_large_tests-t.dir/hypergraph_optimizer-t.cc.o
 25877 ms: ./sql/CMakeFiles/sql_gis.dir/gis/symdifference_functor.cc.o
 25600 ms: ./unittest/gunit/xplugin/xcl/CMakeFiles/xclient_unit_tests.dir/session_options_t.cc.o

**** Templates that took longest to instantiate:
109225 ms: std::__function::__func<(lambda at /Users/laurynas/vilniusdb/mysql-8... (7110 times, avg 15 ms)
107390 ms: std::__function::__func<(lambda at /Users/laurynas/vilniusdb/mysql-8... (7110 times, avg 15 ms)
 93389 ms: std::function<void ()>::function<(lambda at /Users/laurynas/vilniusd... (2370 times, avg 39 ms)
 92573 ms: std::__function::__value_func<void ()>::__value_func<(lambda at /Use... (2370 times, avg 39 ms)
 92539 ms: std::function<void ()>::function<(lambda at /Users/laurynas/vilniusd... (2370 times, avg 39 ms)
 91689 ms: std::__function::__value_func<void ()>::__value_func<(lambda at /Use... (2370 times, avg 38 ms)
 91551 ms: std::__function::__value_func<void ()>::__value_func<(lambda at /Use... (2370 times, avg 38 ms)
 90546 ms: std::__function::__value_func<void ()>::__value_func<(lambda at /Use... (2370 times, avg 38 ms)
 64823 ms: std::__function::__alloc_func<(lambda at /Users/laurynas/vilniusdb/m... (7110 times, avg 9 ms)
 63866 ms: std::__function::__alloc_func<(lambda at /Users/laurynas/vilniusdb/m... (7110 times, avg 8 ms)
 39867 ms: std::__function::__func<(lambda at /Users/laurynas/vilniusdb/mysql-8... (4740 times, avg 8 ms)
 39246 ms: std::__function::__func<(lambda at /Users/laurynas/vilniusdb/mysql-8... (4740 times, avg 8 ms)
 22360 ms: std::unique_ptr<std::unordered_multimap<const MDL_key *, MDL_ticket_... (1016 times, avg 22 ms)
 22285 ms: std::unique_ptr<std::unordered_multimap<const MDL_key *, MDL_ticket_... (1018 times, avg 21 ms)
 22138 ms: std::default_delete<std::unordered_multimap<const MDL_key *, MDL_tic... (1018 times, avg 21 ms)
 17698 ms: std::copy_n<const char16_t *, unsigned long, char16_t *> (3325 times, avg 5 ms)
 17666 ms: testing::internal::ValueArray<bool, bool>::operator ParamGenerator<b... (525 times, avg 33 ms)
 17432 ms: std::copy<const char16_t *, char16_t *> (3326 times, avg 5 ms)
 17359 ms: std::copy_n<const wchar_t *, unsigned long, wchar_t *> (3326 times, avg 5 ms)
 17057 ms: std::copy<const wchar_t *, wchar_t *> (3326 times, avg 5 ms)
 16950 ms: net::basic_waitable_timer<std::chrono::steady_clock>::cancel (150 times, avg 113 ms)
 16946 ms: net::io_context::cancel<net::basic_waitable_timer<std::chrono::stead... (150 times, avg 112 ms)
 16813 ms: std::copy_n<const char *, unsigned long, char *> (3314 times, avg 5 ms)
 16622 ms: std::copy_n<const char32_t *, unsigned long, char32_t *> (3326 times, avg 4 ms)
 16448 ms: std::__copy<const char16_t *, const char16_t *, char16_t *, 0> (3326 times, avg 4 ms)
 16373 ms: std::copy<const char *, char *> (3326 times, avg 4 ms)
 16342 ms: std::copy<const char32_t *, char32_t *> (3326 times, avg 4 ms)
 16110 ms: net::basic_waitable_timer<std::chrono::steady_clock>::~basic_waitabl... (139 times, avg 115 ms)
 16047 ms: std::__copy<const wchar_t *, const wchar_t *, wchar_t *, 0> (3326 times, avg 4 ms)
 15781 ms: std::unordered_multimap<const MDL_key *, MDL_ticket_store::MDL_ticke... (1018 times, avg 15 ms)

**** Template sets that took longest to instantiate:
333347 ms: std::function<$>::function<$> (8367 times, avg 39 ms)
330713 ms: std::__function::__value_func<$>::__value_func<$> (8367 times, avg 39 ms)
270430 ms: std::__function::__func<$>::__func (8367 times, avg 32 ms)
236822 ms: testing::internal::FunctionMocker<$>::Invoke (2370 times, avg 99 ms)
229859 ms: std::__function::__alloc_func<$>::__alloc_func (25098 times, avg 9 ms)
227397 ms: testing::internal::FunctionMocker<$>::InvokeWith (2370 times, avg 95 ms)
174340 ms: std::forward_as_tuple<$> (34235 times, avg 5 ms)
163483 ms: std::unique_ptr<$> (75303 times, avg 2 ms)
145187 ms: std::tuple<$> (45300 times, avg 3 ms)
141977 ms: std::__function::__func<$>::__clone (16733 times, avg 8 ms)
 98501 ms: std::__hash_table<$> (11200 times, avg 8 ms)
 94028 ms: std::allocator_traits<$> (31254 times, avg 3 ms)
 93971 ms: std::__compressed_pair<$>::__compressed_pair<$> (35840 times, avg 2 ms)
 91566 ms: std::unordered_map<$> (8606 times, avg 10 ms)
 90396 ms: std::decay<$> (20916 times, avg 4 ms)
 82788 ms: std::copy<$> (16492 times, avg 5 ms)
 80875 ms: std::map<$> (14763 times, avg 5 ms)
 77163 ms: std::__copy<$> (16493 times, avg 4 ms)
 73875 ms: std::vector<$>::push_back (6296 times, avg 11 ms)
 71983 ms: std::copy_n<$> (13989 times, avg 5 ms)
 71340 ms: stdx::expected<$> (9078 times, avg 7 ms)
 70331 ms: std::__tree<$> (19917 times, avg 3 ms)
 68211 ms: std::vector<$> (32556 times, avg 2 ms)
 67929 ms: std::vector<$>::__push_back_slow_path<$> (6078 times, avg 11 ms)
 62076 ms: std::vector<$>::__swap_out_circular_buffer (8254 times, avg 7 ms)
 57906 ms: std::__decay<$> (12966 times, avg 4 ms)
 54976 ms: std::__compressed_pair<$> (17033 times, avg 3 ms)
 53465 ms: std::unordered_map<$>::unordered_map (4697 times, avg 11 ms)
 53099 ms: testing::internal::MatcherBase<$>::MatcherBase<$> (5591 times, avg 9 ms)
 52731 ms: testing::internal::MatcherBase<$>::Init<$> (5591 times, avg 9 ms)

**** Functions that took longest to compile:
  2560 ms: MYSQLparse(THD*, Parse_tree_root**) (/Users/laurynas/vilniusdb/mysql-8.0.33/_build-release-time-report/sql/sql_yacc.cc)
  1602 ms: _GLOBAL__sub_I_gis_union_t.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/gis_union-t.cc)
  1577 ms: _GLOBAL__sub_I_gis_difference_t.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/gis_difference-t.cc)
  1396 ms: _GLOBAL__sub_I_gis_intersection_t.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/gis_intersection-t.cc)
  1280 ms: _GLOBAL__sub_I_gis_symdifference_t.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/gis_symdifference-t.cc)
   877 ms: __cxx_global_var_init.334 (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/router/tests/test_keyring_frontend.cc)
   839 ms: json_binary_unittest::JsonBinaryTest_BasicTest_Test::TestBody() (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/json_binary-t.cc)
   823 ms: _GLOBAL__sub_I_sys_vars.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/sys_vars.cc)
   821 ms: __cxx_global_var_init.143 (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/router/tests/test_keyring_frontend.cc)
   809 ms: ShareConnectionTinyPoolOneServerTest_not_sharable_Test::TestBody() (/Users/laurynas/vilniusdb/mysql-8.0.33/router/tests/integration/test_routing_sharing_constrained_pools.cc)
   732 ms: testing::internal::FlatTupleBase<testing::internal::FlatTuple<Target... (/Users/laurynas/vilniusdb/mysql-8.0.33/router/tests/component/test_bootstrap_clusterset.cc)
   719 ms: AccountReuseCreateComboTestP::gen_testcases() (/Users/laurynas/vilniusdb/mysql-8.0.33/router/tests/component/test_bootstrap_account.cc)
   689 ms: KeyringFrontendTest_ensure_Test::TestBody() (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/router/tests/test_keyring_frontend.cc)
   624 ms: duk__js_execute_bytecode_inner (/Users/laurynas/vilniusdb/mysql-8.0.33/extra/duktape/duktape-2.7.0/src/duktape.c)
   609 ms: _GLOBAL__sub_I_test_classic_protocol_message.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/mysql_protocol/tests/test_classic_protocol_message.cc)
   591 ms: MySQLRouter::prepare_command_options() (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/router/src/router_app.cc)
   579 ms: json_dom_unittest::JsonDomTest_BasicTest_Test::TestBody() (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/json_dom-t.cc)
   578 ms: SetObjectMembers(std::__1::unique_ptr<Json_object, std::__1::default... (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/join_optimizer/explain_access_path.cc)
   538 ms: operations_unittest::KeyringCommonOperations_test_OperationsTestWith... (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/components/keyring_common/operations-t.cc)
   531 ms: strnxfrm_unittest::StrmxfrmHashTest_HashStability_Test::TestBody() (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/strings_strnxfrm-t.cc)
   511 ms: spec_adder(rapidjson::GenericDocument<rapidjson::UTF8<char>, rapidjs... (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/rest_routing/src/rest_routing_plugin.cc)
   499 ms: mysys_my_time::MysysMyTime_StrToDatetime_Test::TestBody() (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/mysys_my_time-t.cc)
   476 ms: my_strnncoll_uca_900(CHARSET_INFO const*, unsigned char const*, unsi... (/Users/laurynas/vilniusdb/mysql-8.0.33/strings/ctype-uca.cc)
   467 ms: dd_properties_unittest::PropertiesTest_ValidSetGetIntBool_Test::Test... (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/dd_properties-t.cc)
   450 ms: testing::internal::FlatTupleBase<testing::internal::FlatTuple<Target... (/Users/laurynas/vilniusdb/mysql-8.0.33/router/tests/component/test_bootstrap_clusterset.cc)
   444 ms: KeyringManager_init_with_key_file_Test::TestBody() (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/harness/tests/test_keyring_manager.cc)
   427 ms: _GLOBAL__sub_I_hypergraph_optimizer_t.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/hypergraph_optimizer-t.cc)
   425 ms: _GLOBAL__sub_I_admin_cmd_arguments_object_t.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/xplugin/xpl/admin_cmd_arguments_object_t.cc)
   416 ms: __cxx_global_var_init.49 (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/http/tests/test_passwd.cc)
   378 ms: _GLOBAL__sub_I_expr_generator_parametric_t.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/unittest/gunit/xplugin/xpl/expr_generator_parametric_t.cc)

**** Function sets that took longest to compile / optimize:
 39400 ms: testing::internal::FunctionMocker<$>::InvokeWith(std::__1::tuple<$>&&) (1961 times, avg 20 ms)
 21051 ms: testing::internal::TypedExpectation<$>::ExplainMatchResultTo(std::__... (1993 times, avg 10 ms)
 19210 ms: testing::internal::ParameterizedTestSuiteInfo<$>::RegisterTests() (406 times, avg 47 ms)
 18661 ms: void testing::internal::TuplePrefix<$>::ExplainMatchFailuresTo<$>(st... (2627 times, avg 7 ms)
 17837 ms: testing::internal::TypeParameterizedTest<$>::Register(char const*, t... (1286 times, avg 13 ms)
 14176 ms: testing::internal::TypedExpectation<$>::GetCurrentAction(testing::in... (1993 times, avg 7 ms)
 12635 ms: bool boost::geometry::detail::partition::partition_two_ranges<$>::ap... (562 times, avg 22 ms)
 11895 ms: testing::internal::FunctionMocker<$>::PerformDefaultAction(std::__1:... (1961 times, avg 6 ms)
  6589 ms: testing::internal::FunctionMocker<$>::PerformAction(void const*, std... (1961 times, avg 3 ms)
  5890 ms: testing::internal::FunctionMocker<$>::DescribeDefaultActionTo(std::_... (1993 times, avg 2 ms)
  5854 ms: testing::internal::FunctionMocker<$>::PrintTriedExpectationsLocked(s... (1993 times, avg 2 ms)
  5767 ms: testing::internal::FunctionMocker<$>::UntypedFindMatchingExpectation... (1993 times, avg 2 ms)
  5176 ms: std::__1::ostreambuf_iterator<$> std::__1::__pad_and_output<$>(std::... (1053 times, avg 4 ms)
  4801 ms: testing::internal::OnCallSpec<$>::GetAction() const (1961 times, avg 2 ms)
  4660 ms: void std::__1::__introsort<$>(boost::geometry::detail::overlay::turn... (46 times, avg 101 ms)
  4271 ms: testing::internal::TypedExpectation<$>::GetActionForArguments(testin... (1993 times, avg 2 ms)
  3850 ms: std::__1::basic_ostream<$>& std::__1::__put_character_sequence<$>(st... (1053 times, avg 3 ms)
  3807 ms: testing::internal::SuiteApiResolver<$>::GetSetUpCaseOrSuite(char con... (2157 times, avg 1 ms)
  3796 ms: testing::internal::MockSpec<$>::InternalExpectedAt(char const*, int,... (359 times, avg 10 ms)
  3777 ms: std::__1::__function::__func<$>::target(std::type_info const&) const (1456 times, avg 2 ms)
  3254 ms: testing::internal::ParameterizedTestSuiteInfo<$>* testing::internal:... (406 times, avg 8 ms)
  3079 ms: std::__1::__function::__func<$>::__clone() const (906 times, avg 3 ms)
  2966 ms: testing::internal::TestFactoryImpl<$>::CreateTest() (968 times, avg 3 ms)
  2809 ms: classic_protocol::Codec<$>::decode(net::const_buffer const&, std::__... (120 times, avg 23 ms)
  2744 ms: testing::internal::SuiteApiResolver<$>::GetTearDownCaseOrSuite(char ... (2157 times, avg 1 ms)
  2600 ms: testing::internal::MatcherBase<$>::DescribeTo(std::__1::basic_ostrea... (1491 times, avg 1 ms)
  2496 ms: testing::internal::FunctionMocker<$>::ClearDefaultActionsLocked() (846 times, avg 2 ms)
  2447 ms: bool boost::geometry::detail::partition::partition_two_ranges<$>::ap... (104 times, avg 23 ms)
  2442 ms: testing::internal::MatcherBase<$>::~MatcherBase() (588 times, avg 4 ms)
  2394 ms: testing::Matcher<$>::~Matcher() (889 times, avg 2 ms)

*** Expensive headers:
209141 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/sql/sql_class.h (included 658 times, avg 317 ms), included via:
  replicated_columns_view.cc.o replicated_columns_view.h column_filter_factory.h column_filter_inbound_func_indexes.h column_filter.h  (877 ms)
  mysql_connection_attributes_iterator_imp.cc.o  (868 ms)
  mysql_query_attributes_imp.cc.o  (827 ms)
  mysql_thd_attributes_imp.cc.o  (809 ms)
  trx0i_s.cc.o  (806 ms)
  hold_transactions.cc.o hold_transactions.h  (800 ms)
  ...

181774 ms: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/usr/include/c++/v1/__functional/boyer_moore_searcher.h (included 3328 times, avg 54 ms), included via:
  classic_stmt_reset_forwarder.h forwarding_processor.h processor.h basic_protocol_splicer.h functional  (310 ms)
  sql_authorization.h functional  (244 ms)
  functional  (239 ms)
  keycache.h string_view functional  (231 ms)
  geometry.hpp geometry.hpp radian_access.hpp cast.hpp converter.hpp converter_policies.hpp functional  (231 ms)
  json_dom.h functional  (229 ms)
  ...

165255 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/extra/googletest/googletest-release-1.12.0/googletest/include/gtest/gtest.h (included 525 times, avg 314 ms), included via:
  cell_calculator-t.cc.o  (757 ms)
  strings_valid_check-t.cc.o  (717 ms)
  varlen_sort-t.cc.o  (717 ms)
  reference_cache-t.cc.o  (701 ms)
  allocator-t.cc.o  (692 ms)
  val_int_compare-t.cc.o  (691 ms)
  ...

155001 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/include/m_string.h (included 2102 times, avg 73 ms), included via:
  mf_path.cc.o  (598 ms)
  mf_loadpath.cc.o  (558 ms)
  NdbTCP.cpp.o ndb_global.h  (557 ms)
  mf_tempdir.cc.o  (531 ms)
  my_symlink.cc.o  (476 ms)
  mf_same.cc.o my_sys.h  (474 ms)
  ...

125881 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/include/lex_string.h (included 1271 times, avg 99 ms), included via:
  sql_hints.yy.cc.o  (390 ms)
  dd_trigger.cc.o dd_trigger.h  (386 ms)
  rpl_async_conn_failover_table_operations.cc.o log_builtins.h log.h  (382 ms)
  xpl_log.cc.o xpl_log.h log_builtins.h log.h  (364 ms)
  sql_authentication.cc.o sql_authentication.h  (360 ms)
  mysql_audit_print_service_double_data_source_imp.cc.o events.h  (336 ms)
  ...

121427 ms: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/usr/include/c++/v1/__memory/shared_ptr.h (included 3369 times, avg 36 ms), included via:
  vector __split_buffer memory  (134 ms)
  fstream __locale memory  (122 ms)
  fstream __locale memory  (120 ms)
  gtest.h memory  (115 ms)
  ndb_global.h m_string.h algorithm memory  (113 ms)
  geometry.hpp geometry.hpp radian_access.hpp cast.hpp converter.hpp converter_policies.hpp functional boyer_moore_searcher.h  (110 ms)
  ...

117425 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/sql/field.h (included 693 times, avg 169 ms), included via:
  table_access_service.cc.o  (790 ms)
  field.cc.o  (733 ms)
  row.cc.o  (692 ms)
  i_s.cc.o  (692 ms)
  lob0update.cc.o  (618 ms)
  sql_select.cc.o sql_select.h  (585 ms)
  ...

107213 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/extra/googletest/googletest-release-1.12.0/googlemock/include/gmock/gmock.h (included 271 times, avg 395 ms), included via:
  socket_acceptor_task_t.cc.o  (907 ms)
  timeouts_t.cc.o  (829 ms)
  gmock-all.cc.o  (813 ms)
  sasl_plain_auth_t.cc.o  (787 ms)
  sha256_cache_t.cc.o  (783 ms)
  gcs_xcom_control_interface-t.cc.o gcs_base_test.h  (782 ms)
  ...

107076 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/sql/table.h (included 845 times, avg 126 ms), included via:
  dd_routine.cc.o dd_routine.h  (718 ms)
  global.cc.o global.h  (700 ms)
  table.cc.o  (664 ms)
  rpl_sys_key_access.cc.o rpl_sys_key_access.h  (619 ms)
  zlob0update.cc.o  (575 ms)
  pfs_instr_class.cc.o  (396 ms)
  ...

102310 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/sql/handler.h (included 923 times, avg 110 ms), included via:
  page_track_service.cc.o  (717 ms)
  ha_example.cc.o ha_example.h  (668 ms)
  ha_archive.cc.o ha_archive.h  (631 ms)
  ha_mock.cc.o ha_mock.h  (625 ms)
  ha_perfschema.cc.o ha_perfschema.h  (624 ms)
  ha_blackhole.cc.o ha_blackhole.h  (598 ms)
  ...

  done in 1.4s.


Nice, isn'it? Now what can we find in there?

  • "Files that took longest to parse (compiler frontend)": all tests
  • "Files that took longest to codegen (compiler backend)": tests again, with a bit of GIS code (no surprise there because Boost is used there)
  • "Templates that took longest to instantiate": lots of truncated output of std::function (cannot tell where used because truncation and too lazy to rerun the analyzer with the longer output option)
  • "Template sets that took longest to instantiate": std::function again, Google Test, and regular C++ standard library things.
  • "Functions that took longest to compile": MYSQLparse (no surprise there), GIS code (again no surprise), tests (by now no surprise neither)
  • "Function sets that took longest to compile / optimize:": tests tests tests with a bit of GIS
  • "Expensive headers": sql_class.h (I knew it!), C++ library (why so much of shared_ptr BTW?), and tests again.

Let's not compile the tests, then

MySQL CMake allows setting -DWITH_UNIT_TESTS=OFF, which presumably should get rid of most things in the above. Of course, not compiling the unit tests has the inconvenience of not being able to run them after a build, and breaking the build for Mark. But let's experiment.

Rerunning the above with -DWITH_UNIT_TESTS=OFF cut the wall compilation time almost in half, which is unfortunately invalidated by Chrome slowly making its way to a crash in the first third of the full compilation, and the report now looks very different:


**** Time summary:
Compilation (4955 times):
  Parsing (frontend):         2912.1 s
  Codegen & opts (backend):   1280.0 s

**** Files that took longest to parse (compiler frontend):
  7134 ms: ./sql/CMakeFiles/sql_gis.dir/gis/union_functor.cc.o
  6966 ms: ./router/src/router/src/CMakeFiles/router_frontend_lib.dir/router_app.cc.o
  6820 ms: ./sql/CMakeFiles/sql_gis.dir/gis/symdifference_functor.cc.o
  6527 ms: ./sql/CMakeFiles/sql_gis.dir/gis/difference_functor.cc.o
  6454 ms: ./sql/CMakeFiles/sql_gis.dir/gis/intersection_functor.cc.o
  5857 ms: ./sql/CMakeFiles/sql_gis.dir/gis/srs/wkt_parser.cc.o
  5748 ms: ./sql/CMakeFiles/sql_gis.dir/gis/symdifference_functor.cc.o
  5577 ms: ./sql/CMakeFiles/sql_main.dir/auth/sql_authorization.cc.o
  5543 ms: ./sql/CMakeFiles/sql_gis.dir/gis/within.cc.o
  5132 ms: ./sql/CMakeFiles/sql_gis.dir/item_geofunc.cc.o

**** Files that took longest to codegen (compiler backend):
 32200 ms: ./sql/CMakeFiles/sql_gis.dir/gis/intersection_functor.cc.o
 30934 ms: ./sql/CMakeFiles/sql_gis.dir/gis/difference_functor.cc.o
 26649 ms: ./sql/CMakeFiles/sql_gis.dir/gis/symdifference_functor.cc.o
 26390 ms: ./sql/CMakeFiles/sql_gis.dir/gis/touches.cc.o
 23451 ms: ./sql/CMakeFiles/sql_gis.dir/gis/crosses.cc.o
 21500 ms: ./sql/CMakeFiles/sql_gis.dir/gis/within.cc.o
 19349 ms: ./sql/CMakeFiles/sql_gis.dir/gis/distance_functor.cc.o
 19187 ms: ./sql/CMakeFiles/sql_gis.dir/gis/union_functor.cc.o
 18280 ms: ./sql/CMakeFiles/sql_gis.dir/gis/overlaps.cc.o
 16962 ms: ./sql/CMakeFiles/sql_gis.dir/gis/buffer.cc.o

**** Templates that took longest to instantiate:
 20773 ms: std::unique_ptr<std::unordered_multimap<const MDL_key *, MDL_ticket_... (914 times, avg 22 ms)
 20707 ms: std::unique_ptr<std::unordered_multimap<const MDL_key *, MDL_ticket_... (915 times, avg 22 ms)
 20550 ms: std::default_delete<std::unordered_multimap<const MDL_key *, MDL_tic... (915 times, avg 22 ms)
 14289 ms: std::unordered_multimap<const MDL_key *, MDL_ticket_store::MDL_ticke... (915 times, avg 15 ms)
 12743 ms: std::copy_n<const wchar_t *, unsigned long, wchar_t *> (2547 times, avg 5 ms)
 12637 ms: std::copy_n<const char *, unsigned long, char *> (2542 times, avg 4 ms)
 12551 ms: std::copy<const wchar_t *, wchar_t *> (2547 times, avg 4 ms)
 12346 ms: std::copy_n<const char16_t *, unsigned long, char16_t *> (2546 times, avg 4 ms)
 12307 ms: std::copy<const char *, char *> (2547 times, avg 4 ms)
 12145 ms: std::copy<const char16_t *, char16_t *> (2547 times, avg 4 ms)
 11944 ms: std::__copy<const wchar_t *, const wchar_t *, wchar_t *, 0> (2547 times, avg 4 ms)
 11894 ms: std::copy_n<const char32_t *, unsigned long, char32_t *> (2547 times, avg 4 ms)
 11674 ms: std::copy<const char32_t *, char32_t *> (2547 times, avg 4 ms)
 11667 ms: std::__copy<const char *, const char *, char *, 0> (2547 times, avg 4 ms)
 11526 ms: std::__copy<const char16_t *, const char16_t *, char16_t *, 0> (2547 times, avg 4 ms)
 10919 ms: std::__copy<const char32_t *, const char32_t *, char32_t *, 0> (2547 times, avg 4 ms)
 10219 ms: collation_unordered_map<std::string, std::unique_ptr<user_var_entry,... (600 times, avg 17 ms)
  9859 ms: std::basic_string<char>::basic_string (2726 times, avg 3 ms)
  9834 ms: std::vector<unsigned char *>::push_back (784 times, avg 12 ms)
  9728 ms: malloc_unordered_map<char **, std::unique_ptr<char, My_free_deleter>... (601 times, avg 16 ms)
  9355 ms: std::vector<unsigned char *>::__push_back_slow_path<unsigned char *c... (784 times, avg 11 ms)
  9318 ms: std::unordered_map<std::string, unsigned long> (846 times, avg 11 ms)
  8973 ms: std::unordered_map<std::string, std::unique_ptr<user_var_entry, void... (600 times, avg 14 ms)
  8932 ms: std::basic_string<wchar_t>::basic_string (2611 times, avg 3 ms)
  8793 ms: std::__scalar_hash<std::_PairT, 2>::operator() (2580 times, avg 3 ms)
  8642 ms: net::basic_waitable_timer<std::chrono::steady_clock>::cancel (81 times, avg 106 ms)
  8639 ms: net::io_context::cancel<net::basic_waitable_timer<std::chrono::stead... (81 times, avg 106 ms)
  8568 ms: collation_unordered_map<std::string, std::unique_ptr<Table_ref, My_f... (600 times, avg 14 ms)
  8505 ms: std::basic_string<char16_t>::basic_string (2609 times, avg 3 ms)
  8493 ms: std::basic_string<char32_t>::basic_string (2618 times, avg 3 ms)

**** Template sets that took longest to instantiate:
 91311 ms: std::unique_ptr<$> (44993 times, avg 2 ms)
 81831 ms: std::__hash_table<$> (9702 times, avg 8 ms)
 74391 ms: std::unordered_map<$> (7352 times, avg 10 ms)
 64459 ms: std::allocator_traits<$> (22807 times, avg 2 ms)
 62238 ms: std::map<$> (11577 times, avg 5 ms)
 59739 ms: std::function<$>::function<$> (1549 times, avg 38 ms)
 59342 ms: std::__function::__value_func<$>::__value_func<$> (1549 times, avg 38 ms)
 54833 ms: std::copy<$> (11586 times, avg 4 ms)
 54215 ms: std::__tree<$> (15582 times, avg 3 ms)
 51692 ms: std::__copy<$> (11585 times, avg 4 ms)
 50051 ms: std::copy_n<$> (10264 times, avg 4 ms)
 48536 ms: std::decay<$> (11965 times, avg 4 ms)
 48319 ms: std::__function::__func<$>::__func (1549 times, avg 31 ms)
 48146 ms: std::vector<$>::push_back (3923 times, avg 12 ms)
 45687 ms: std::unordered_map<$>::unordered_map (3885 times, avg 11 ms)
 45017 ms: std::vector<$>::__push_back_slow_path<$> (3822 times, avg 11 ms)
 44368 ms: std::vector<$> (21423 times, avg 2 ms)
 41048 ms: std::vector<$>::__swap_out_circular_buffer (5253 times, avg 7 ms)
 40485 ms: std::__function::__alloc_func<$>::__alloc_func (4647 times, avg 8 ms)
 38700 ms: std::basic_string<$>::basic_string (11728 times, avg 3 ms)
 36048 ms: std::unique_ptr<$>::reset (4008 times, avg 8 ms)
 35840 ms: std::pair<$> (16452 times, avg 2 ms)
 35779 ms: std::__hash_table<$>::__hash_table (4198 times, avg 8 ms)
 35340 ms: std::unique_ptr<$>::~unique_ptr (3870 times, avg 9 ms)
 34007 ms: std::__compressed_pair<$> (10756 times, avg 3 ms)
 33242 ms: std::__uninitialized_allocator_move_if_noexcept<$> (5252 times, avg 6 ms)
 33225 ms: std::basic_string<$> (11487 times, avg 2 ms)
 32465 ms: std::forward_as_tuple<$> (6724 times, avg 4 ms)
 31334 ms: malloc_unordered_map<$> (3118 times, avg 10 ms)
 30751 ms: std::__decay<$> (7475 times, avg 4 ms)

**** Functions that took longest to compile:
  2514 ms: MYSQLparse(THD*, Parse_tree_root**) (/Users/laurynas/vilniusdb/mysql-8.0.33/_build-release-time-report-no-unit-tests/sql/sql_yacc.cc)
   819 ms: _GLOBAL__sub_I_sys_vars.cc (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/sys_vars.cc)
   592 ms: duk__js_execute_bytecode_inner (/Users/laurynas/vilniusdb/mysql-8.0.33/extra/duktape/duktape-2.7.0/src/duktape.c)
   582 ms: SetObjectMembers(std::__1::unique_ptr<Json_object, std::__1::default... (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/join_optimizer/explain_access_path.cc)
   510 ms: MySQLRouter::prepare_command_options() (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/router/src/router_app.cc)
   456 ms: my_strnncoll_uca_900(CHARSET_INFO const*, unsigned char const*, unsi... (/Users/laurynas/vilniusdb/mysql-8.0.33/strings/ctype-uca.cc)
   429 ms: spec_adder(rapidjson::GenericDocument<rapidjson::UTF8<char>, rapidjs... (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/rest_routing/src/rest_routing_plugin.cc)
   384 ms: init_handlers(mysql_harness::PluginFuncEnv*, mysql_harness::LoaderCo... (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/harness/src/logging/logger_plugin.cc)
   351 ms: rapidjson::internal::Schema<rapidjson::GenericSchemaDocument<rapidjs... (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/harness/src/dynamic_state.cc)
   304 ms: my_strnxfrm_uca_900(CHARSET_INFO const*, unsigned char*, unsigned lo... (/Users/laurynas/vilniusdb/mysql-8.0.33/strings/ctype-uca.cc)
   276 ms: spec_adder(rapidjson::GenericDocument<rapidjson::UTF8<char>, rapidjs... (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/rest_metadata_cache/src/rest_metadata_cache_plugin.cc)
   271 ms: my_hash_sort_uca_900(CHARSET_INFO const*, unsigned char const*, unsi... (/Users/laurynas/vilniusdb/mysql-8.0.33/strings/ctype-uca.cc)
   250 ms: test_sql(void*) (/Users/laurynas/vilniusdb/mysql-8.0.33/plugin/test_service_sql_api/test_sql_lock.cc)
   247 ms: CmdArgHandler::process(std::__1::vector<std::__1::basic_string<char,... (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/harness/src/arg_handler.cc)
   242 ms: open_table_def(THD*, TABLE_SHARE*, dd::Table const&) (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/dd_table_share.cc)
   237 ms: trx_undo_report_row_operation(unsigned long, unsigned long, que_thr_... (/Users/laurynas/vilniusdb/mysql-8.0.33/storage/innobase/trx/trx0rec.cc)
   230 ms: test_wl4435_3() (/Users/laurynas/vilniusdb/mysql-8.0.33/testclients/mysql_client_test.cc)
   212 ms: void std::__1::__introsort<std::__1::_ClassicAlgPolicy, boost::geome... (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/gis/overlaps.cc)
   211 ms: dd::tables::Tables::Tables() (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/dd/impl/tables/tables.cc)
   210 ms: spec_adder(rapidjson::GenericDocument<rapidjson::UTF8<char>, rapidjs... (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/rest_connection_pool/src/rest_connection_pool_plugin.cc)
   209 ms: TlsServerContext::default_ciphers() (/Users/laurynas/vilniusdb/mysql-8.0.33/router/src/harness/src/tls_server_context.cc)
   209 ms: main (/Users/laurynas/vilniusdb/mysql-8.0.33/client/mysqltest.cc)
   207 ms: build_gcs_parameters(Gcs_interface_parameters&) (/Users/laurynas/vilniusdb/mysql-8.0.33/plugin/group_replication/src/plugin.cc)
   205 ms: init_server_components() (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/mysqld.cc)
   202 ms: void std::__1::__introsort<std::__1::_ClassicAlgPolicy, boost::geome... (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/gis/overlaps.cc)
   200 ms: ConnectJoins(int, int, int, QEP_TAB*, THD*, CallingContext, std::__1... (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/sql_executor.cc)
   194 ms: lock_wait_timeout_thread() (/Users/laurynas/vilniusdb/mysql-8.0.33/storage/innobase/lock/lock0wait.cc)
   193 ms: void std::__1::__introsort<std::__1::_ClassicAlgPolicy, boost::geome... (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/gis/crosses.cc)
   187 ms: (anonymous namespace)::CostingReceiver::FoundSingleNode(int) (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/join_optimizer/join_optimizer.cc)
   186 ms: handle_slave_io (/Users/laurynas/vilniusdb/mysql-8.0.33/sql/rpl_replica.cc)

**** Function sets that took longest to compile / optimize:
 13251 ms: bool boost::geometry::detail::partition::partition_two_ranges<$>::ap... (562 times, avg 23 ms)
  4962 ms: void std::__1::__introsort<$>(boost::geometry::detail::overlay::turn... (46 times, avg 107 ms)
  2395 ms: bool boost::geometry::detail::partition::partition_two_ranges<$>::ap... (104 times, avg 23 ms)
  2264 ms: std::__1::ostreambuf_iterator<$> std::__1::__pad_and_output<$>(std::... (536 times, avg 4 ms)
  1943 ms: void std::__1::__introsort<$>(boost::geometry::detail::relate::linea... (26 times, avg 74 ms)
  1717 ms: std::__1::basic_ostream<$>& std::__1::__put_character_sequence<$>(st... (536 times, avg 3 ms)
  1657 ms: std::__1::basic_stringbuf<$>::str() const (180 times, avg 9 ms)
  1298 ms: unsigned int std::__1::__sort3<$>(boost::geometry::detail::overlay::... (46 times, avg 28 ms)
  1248 ms: bool boost::geometry::partition<$>::apply<$>(boost::geometry::sectio... (281 times, avg 4 ms)
  1224 ms: unsigned int std::__1::__sort5<$>(boost::geometry::detail::overlay::... (46 times, avg 26 ms)
  1220 ms: void std::__1::__tree_balance_after_insert<$>(std::__1::__tree_node_... (317 times, avg 3 ms)
  1211 ms: std::__1::deque<$>::__add_back_capacity() (94 times, avg 12 ms)
  1049 ms: bool boost::geometry::detail::overlay::get_turn_info_for_endpoint<$>... (52 times, avg 20 ms)
   991 ms: bool boost::geometry::detail::partition::partition_one_range<$>::app... (104 times, avg 9 ms)
   968 ms: spec_adder(rapidjson::GenericDocument<$>&) (4 times, avg 242 ms)
   968 ms: std::__1::__tree_node_base<$>*& std::__1::__tree<$>::__find_equal<$>... (253 times, avg 3 ms)
   956 ms: bool boost::geometry::detail::partition::partition_two_ranges<$>::ap... (64 times, avg 14 ms)
   939 ms: (anonymous namespace)::Function_factory<$>::create_func(THD*, MYSQL_... (270 times, avg 3 ms)
   938 ms: std::__1::basic_stringbuf<$>::overflow(int) (180 times, avg 5 ms)
   922 ms: classic_protocol::Codec<$>::decode(net::const_buffer const&, std::__... (70 times, avg 13 ms)
   884 ms: bool boost::geometry::detail::partition::partition_two_ranges<$>::ap... (40 times, avg 22 ms)
   866 ms: boost::geometry::policies::relate::segments_intersection_policy<$>::... (30 times, avg 28 ms)
   864 ms: std::__1::basic_string<$>::basic_string[abi:v15006]<$>(char const*) (351 times, avg 2 ms)
   837 ms: rapidjson::internal::Schema<$>::Schema(rapidjson::GenericSchemaDocum... (5 times, avg 167 ms)
   814 ms: bool std::__1::__insertion_sort_incomplete<$>(boost::geometry::detai... (46 times, avg 17 ms)
   813 ms: void std::__1::__introsort<$>(boost::geometry::detail::relate::linea... (14 times, avg 58 ms)
   810 ms: std::__1::__tree<$>::destroy(std::__1::__tree_node<$>*) (443 times, avg 1 ms)
   808 ms: unsigned int std::__1::__sort4<$>(boost::geometry::detail::overlay::... (46 times, avg 17 ms)
   795 ms: void std::__1::__tree_remove<$>(std::__1::__tree_node_base<$>*, std:... (90 times, avg 8 ms)
   788 ms: std::__1::vector<$>::~vector[abi:v15006]() (418 times, avg 1 ms)

*** Expensive headers:
181934 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/sql/sql_class.h (included 587 times, avg 309 ms), included via:
  mysql_thd_attributes_imp.cc.o  (936 ms)
  replicated_columns_view.cc.o replicated_columns_view.h column_filter_factory.h column_filter_inbound_func_indexes.h column_filter.h  (910 ms)
  mysql_connection_attributes_iterator_imp.cc.o  (838 ms)
  ndb_create_helper.cc.o  (812 ms)
  sql_class.cc.o  (810 ms)
  replicated_columns_view_with_gipk_on_source.cc.o replicated_columns_view_with_gipk_on_source.h replicated_columns_view.h column_filter_factory.h column_filter_inbound_func_indexes.h column_filter.h  (771 ms)
  ...

140903 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/include/m_string.h (included 1793 times, avg 78 ms), included via:
  my_print_defaults.cc.o  (428 ms)
  my_strtoll10.cc.o  (412 ms)
  file.cc.o buf0checksum.h buf0types.h os0event.h univ.i  (406 ms)
  NdbThread.cpp.o ndb_global.h  (389 ms)
  AccLock.cpp.o AccLock.hpp SignalData.hpp ndb_global.h  (386 ms)
  NdbReceiver.cpp.o API.hpp ndb_global.h  (382 ms)
  ...

129393 ms: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/usr/include/c++/v1/__functional/boyer_moore_searcher.h (included 2549 times, avg 50 ms), included via:
  bgc_ticket_manager.h atomic_bgc_ticket_guard.h functional  (231 ms)
  acl_table_user.h functional  (228 ms)
  config_parser.h functional  (217 ms)
  dynamic_privilege_table.h functional  (217 ms)
  loader_config.h config_parser.h functional  (210 ms)
  random_generator.h random discrete_distribution.h numeric functional  (208 ms)
  ...

114343 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/include/lex_string.h (included 1195 times, avg 95 ms), included via:
  sql_authentication.cc.o sql_authentication.h  (329 ms)
  audit_api_connection_service_imp.cc.o sql_audit.h  (308 ms)
  dd_trigger.cc.o dd_trigger.h  (305 ms)
  sql_hints.yy.cc.o  (299 ms)
  sql_user_table.cc.o sql_user_table.h sql_system_table_check.h log_builtins.h log.h  (297 ms)
  show_query_builder.cc.o show_query_builder.h  (292 ms)
  ...

105206 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/sql/field.h (included 634 times, avg 165 ms), included via:
  table_access_service.cc.o  (733 ms)
  rpl_sys_table_access.cc.o rpl_sys_table_access.h  (611 ms)
  sql_select.cc.o sql_select.h  (597 ms)
  field.cc.o  (597 ms)
  row.cc.o  (581 ms)
  lob0update.cc.o  (577 ms)
  ...

95348 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/sql/table.h (included 779 times, avg 122 ms), included via:
  dd_routine.cc.o dd_routine.h  (633 ms)
  global.cc.o global.h  (582 ms)
  table.cc.o  (551 ms)
  zlob0update.cc.o  (493 ms)
  rpl_sys_key_access.cc.o rpl_sys_key_access.h  (488 ms)
  table_replication_group_member_actions.cc.o rpl_sys_key_access.h  (369 ms)
  ...

94011 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/storage/innobase/include/univ.i (included 209 times, avg 449 ms), included via:
  mach0data.cc.o mach0data.h mtr0types.h sync0rw.h  (710 ms)
  buf0block_hint.cc.o buf0block_hint.h buf0types.h os0event.h  (705 ms)
  btr0sea.cc.o btr0sea.h  (700 ms)
  srv0tmp.cc.o srv0tmp.h srv0srv.h buf0checksum.h buf0types.h os0event.h  (684 ms)
  file.cc.o buf0checksum.h buf0types.h os0event.h  (681 ms)
  btr0btr.cc.o btr0btr.h btr0types.h  (674 ms)
  ...

93077 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/sql/log_event.h (included 241 times, avg 386 ms), included via:
  global.cc.o global.h context.h  (889 ms)
  log_event.cc.o  (879 ms)
  context.cc.o context.h  (862 ms)
  recovery.cc.o recovery.h global.h context.h  (811 ms)
  iterators.cc.o iterators.h binlog_reader.h  (775 ms)
  binlog_istream.cc.o  (775 ms)
  ...

91706 ms: /Users/laurynas/vilniusdb/mysql-8.0.33/sql/handler.h (included 842 times, avg 108 ms), included via:
  ha_example.cc.o ha_example.h  (756 ms)
  ha_mock.cc.o ha_mock.h  (685 ms)
  page_track_service.cc.o  (649 ms)
  ha_tina.cc.o ha_tina.h  (614 ms)
  ha_archive.cc.o ha_archive.h  (580 ms)
  api0misc.cc.o api0misc.h  (575 ms)
  ...

87795 ms: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk/usr/include/c++/v1/__memory/shared_ptr.h (included 2580 times, avg 34 ms), included via:
  AccLock.hpp SignalData.hpp ndb_global.h m_string.h algorithm memory  (102 ms)
  dynamic_privilege_table.h functional boyer_moore_searcher.h  (98 ms)
  signing_key.h string memory  (97 ms)
  ndb_global.h m_string.h algorithm memory  (96 ms)
  dict0dict.h set __node_handle memory  (93 ms)
  my_byteorder.h template_utils.h algorithm memory  (93 ms)
  ...

  done in 0.8s.


What changed?

  • "Files that took longest to parse (compiler frontend)": all GIS
  • "Files that took longest to codegen (compiler backend)": all GIS
  • "Templates that took longest to instantiate": all those std::function gone, so must have been tests. Something about MDL remaining.
  • "Template sets that took longest to instantiate": std::unique_ptr. Oh well.
  • "Functions that took longest to compile": MYSQLparse (no surprise there), system variables handling, 3rd party code, and variety of other things
  • "Function sets that took longest to compile / optimize:": Boost/GIS, the standard library.
  • "Expensive headers": sql_class.h, and the C++ standard library headers mostly gone together with shared_ptr (so it was tests using it).

Interestingly (and usefully) this provides a different kind of insight than what Mark discovered for Meta branch: it would not discover RocksDB being compiled three times. It would be great if the MySQL developers looked into this tool, which seems very easy to use, and reducing compilation times. That sql_class.h file is way overdue for splitting!

Tuesday, February 07, 2023

Changes for the MySQL clone plugin to support other storage engines

In case you ever wondered what is needed for the Oracle MySQL clone plugin to support other transactional engines than InnoDB, I got you covered: https://bugs.mysql.com/bug.php?id=109926

Wednesday, January 18, 2023

On Writing to the MySQL Error Log

If you develop a 3rd party MySQL plugin, it might take a while for you to figure out how to make your plugin write to the server error log in an official supported way. Let's take a look.

First things first: you don't want to patch the core server and ideally you'd develop and distribute the plugin only. I don't think there exists a single plugin that pulls this off today, but that's the ideal.

Then, you want to be a good citizen and not write to stderr directly. By doing that, you'd bypass the whole error logging machinery and the plugin messages could not be filtered, redirected to i.e. JSON or system error log sinks, and would not have proper severity and timestamps set. As the plugin developer you also want to be productive and avoid avoidable bugs at least, so even if you wanted to write to stderr directly, both printf and std::cerr are terrible APIs. For the former, fixing -Wformat-security warnings for all the platforms results in unreadable format strings "Foo: %" PRIu64 ", bar: %" PRIu64, the latter is just universally terrible with the sole good property of being type-safe. You'd want std::format (or {fmt} in C++17), so you could be ergonomic and type safe at the same time: "Foo: {}, bar: {}".

OK, so you have to use official MySQL APIs for plugins to write to the error logs. Which ones? Well, the internal documentation has a nice chapter "MySQL Services for Plugins" with an example:

my_plugin_log_message(plugin_ptr, MY_ERROR_LEVEL, "Cannot initialize plugin");

That's not too bad, you think, except that this is a printf-like API and not std::format. Then you grep the source tree for this function, and whoa:

/**
For use by legacy sql_print_*(), legacy my_plugin_log_message();
also available via the log_builtins service as message().
Wherever possible, use the fluent C++ wrapper LogErr()
(see log_builtins.h) instead.
*/

Oh, so my_plugin_log_message is "legacy" and you should be using fluent C++ LogErr from log_builtins.h. Let's go there:

#define LogErr(severity, ecode, ...) \
  LogEvent()                         \
      .prio(severity)                \
      .errcode(ecode)                \
      .subsys(LOG_SUBSYSTEM_TAG)     \
      .component(LOG_COMPONENT_TAG)  \
      .source_line(__LINE__)         \
      .source_file(MY_BASENAME)      \
      .function(__FUNCTION__)        \
      .lookup(ecode, ##__VA_ARGS__)

The fluency here applies to the macro definition itself, and not to the macro calls. There is no usage comment for it and there are a lot of things going on that need figuring out, maybe let's try grepping for examples? Grep shows that this macro is indeed heavily used, and also that it is not specific for plugins, because the core server uses it too. So you focus the grep to the plugin/ directory and pick a (not really) random sample:

    mysql_error_service_emit_printf(svc_error, ER_GRP_RPL_UDF_ERROR, MYF(0),
                                    action_name, error_message);
    if (log_error)
      LogErr(ERROR_LEVEL, ER_GRP_RPL_SERVER_UDF_ERROR, action_name,
             error_message);
    return false;
  }
}
// Log the error in case we can't do much
LogPluginErr(ERROR_LEVEL, ER_GRP_RPL_SERVER_UDF_ERROR, action_name,
             error_message);

Many, many things going on here. Why there are LogErr and LogPluginErr both at the same time? Is it the latter you should be using? What's that mysql_error_service_emit_printf thing? It looks error-log'ish too. Finally, why there is both ER_... something and error_message?

Let's start with LogErr vs LogPluginErr. Remember that my_plugin_log_message was documented in the internals manual? These two are not there. Googling gives skeleton Doxygen docs for log_builtins.h, so might as well go to the source code directly:

#define LogPluginErr(severity, ecode, ...)                           \
  LogEvent()                                                         \
      .prio(severity)                                                \
      .errcode(ecode)                                                \
      .subsys(LOG_SUBSYSTEM_TAG)                                     \
      .component("plugin:" LOG_COMPONENT_TAG)                        \
      .source_line(__LINE__)                                         \
      .source_file(MY_BASENAME)                                      \
      .function(__FUNCTION__)                                        \
      .lookup_quoted(ecode, "Plugin " LOG_COMPONENT_TAG " reported", \
                     ##__VA_ARGS__)

That's not too different from the LogErr definition above and seems interchangeable, with the difference being all those extra "plugin:" strings. If you have to choose between LogErr and LogErrPlugin, the latter looks like the right choice. Grepping shows heavy usage in plugin/ directory (and none in the core server), which seems correct. Let's figure out LOG_SUBSYSTEM_TAG and LOG_COMPONENT_TAG (and maybe MY_BASENAME) later.

Next question, should you be concerned about mysql_error_service_emit_printf? The Doxygen docs say "This function is substitute api service for my_error function." And my_error Doxygen docs say "Fill in and print a previously registered error message.". Print where? To an error log? You grep grep grep and half an hour later find yourself in my_message_sql and convince yourself that this error is, in fact, printed to the client and not to the error log. OK, so that was a digression.

At this point you are very certain you should be using LogPluginErr, but it was taking those ER_... error codes and extra parameters. How do you allocate an error code for your 3rd party, out-of-tree plugin? Maybe let's see how the in-tree plugins do that as for the starting point. You grep to find this in share/messages_to_error_log.txt:

ER_GRP_RPL_SERVER_UDF_ERROR
  eng "The function '%s' failed. %s"

You learn that these are printf-style format strings determining the tail of arguments for LogPluginErr. But remember, you don't want to patch the core server. So maybe the build process for this file could pick up some extra files too? You look at comp_err source and how it is invoked by CMake currently, and arrive at the conclusion, that no, you cannot do that.

And there is a fundamental reason: what error code numbers should be assigned to your plugin, so that there are no conflicts with the core server and all the other 3rd party plugins in the world? Ideally without a centralized number assignment authority? The closest I have to the answer is that the plugins could use the letter prefixes, i.e. the core server already prefixes all error codes with MY-. But the prefixes only reduce the conflict probability, not eliminate it.

So at this point you know you are supposed to use LogPluginErr but you can only use the existing error codes for it. How to find some suitable ones? Well a loophole happens to exist:

ER_LOG_PRINTF_MSG
  eng "%s"

An error code that says "this is a printf-formatted message", perfect.

You are almost there, what's remaining are the LOG_SUBSYSTEM_TAG, LOG_COMPONENT_TAG, and MY_BASENAME macros. By grepping for the first two you find something like this, in the group replication plugin:

ADD_DEFINITIONS(-DLOG_SUBSYSTEM_TAG="Repl")
ADD_DEFINITIONS(-DLOG_COMPONENT_TAG="group_replication")

OK, so LOG_COMPONENT_TAG should be defined to your plugin name, and LOG_SUBSYSTEM_TAG to something broader if applicable. And MY_BASENAME takes care of itself: log_builtins.h includes log_shared.h, which includes my_basename.h, which defines it.

Looks like you got all the pieces! Let's try it!

#define LOG_SUBSYSTEM_TAG "ThePlugin"  // This is proof-of-concept only:
#define LOG_COMPONENT_TAG "ThePlugin"  // should be done in CMake
// ...
#include "mysql/components/services/log_builtins.h"
// ...
LogPluginErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG, "Hello, world!");

And the error log:

2023-01-18T06:38:01Z UTC - mysqld got signal 11 ;
...
0   mysqld                              0x0000000102610b8c my_print_stacktrace(unsigned char const*, unsigned long) + 72
1   mysqld                              0x0000000100f47250 print_fatal_signal(int) + 764
2   mysqld                              0x0000000100f4751c handle_fatal_signal + 120
3   libsystem_platform.dylib            0x00000001962fb2a4 _sigtramp + 56
4   ha_theplugin.so                     0x0000000124724c8c LogEvent::LogEvent() + 28
5   ha_theplugin.so                     0x0000000124724c8c LogEvent::LogEvent() + 28
....

Well, this is the error log all right, just not what you asked it to log.

 * thread #41, name = 'connection', stop reason = EXC_BAD_ACCESS (code=1, address=0x80)
    frame #0: 0x0000000151405980 ha_theplugin.so`LogEvent::LogEvent(this=0x00000001717e0308) at log_builtins.h:968:15
   965 	  */
   966 	  LogEvent() {
   967 	    have_msg = false;
-> 968 	    if ((ll = log_line_init()) != nullptr) {
   969 	      if ((msg = (char *)log_malloc(LOG_BUFF_MAX)) == nullptr) {
   970 	        log_line_exit(ll);
   971 	        ll = nullptr;

log_line_init is a nullptr. It looks like a function/method call but there is a macro to expand first:

#define log_line_init log_bi->line_init

And then

(lldb) p log_bi
(const mysql_service_log_builtins_t *) $0 = nullptr

More grepping, this time for log_bi. It is also helpful if you read the internals manual and recall something vague that everything is dynamic now. So let's say you find the following in the rewriter plugin:

static SERVICE_TYPE(registry) *reg_srv = nullptr;
SERVICE_TYPE(log_builtins) *log_bi = nullptr;
SERVICE_TYPE(log_builtins_string) *log_bs = nullptr;
// ...
static int rewriter_plugin_init(MYSQL_PLUGIN plugin_ref) {
  // ...
  // Initialize error logging service.
  if (init_logging_service_for_plugin(&reg_srv, &log_bi, &log_bs)) return 1;
  // ...
}
static int rewriter_plugin_deinit(void *) {
  // ...
  deinit_logging_service_for_plugin(&reg_srv, &log_bi, &log_bs);
  // ...
}

You add the corresponding code to your plugin, and finally:

2023-01-18T06:56:40.735486Z 9 [Note] [MY-011071] [Server] Plugin ThePlugin reported: 'Hello, world!'

Success!

Can you do better? What if you need to log something else than a single static string? Yes, you can, and I am advocating for introducing std::format or {fmt} into the core server here too:

LogPluginErr(ERROR_LEVEL, ER_LOG_PRINTF_MSG,
             fmt::format("An error has occurred while reading {}: errno = {}",
                         path, my_errno()).c_str());

So, here we are. Of course, if you already happen to know that you should be using LogPluginErr(ER_LOG_PRINTF_MSG) or even just my_plugin_log_message then the majority of this post might seem to be trivial. But I believe that documentation and lowering the entry barrier for new MySQL plugin writers is important too.

Wednesday, December 21, 2022

The Zoo of MySQL Storage Engine Flags

MySQL has had pluggable storage engine support for decades now. Their capabilities vary wildly. Some of those storage engines are not even storage engine-like, for example BLACKHOLE does not store data at all. For OLTP, this herd thins very quickly if we only consider ACID-supporting ones: InnoDB, MyRocks, NDB, (up until recently) TokuDB, and (up until ten years ago) PBXT. I am not too familiar with OLAP, but I know there's Warp.

The core server layer has accumulated dozens of engine feature flags over those decades to accommodate those varying capabilities, let's take a look at what they can be. To make this post look less like a source code annotation dump, I'll try to sort them by feature area. If I classify something as an "implementation detail", it still could be more than that, there could be e.g. performance implications or something user-visible like forced closing of a table, but I'll be not figuring that out at this time.

Handlerton Flags

To recap, a "handlerton" is a "handler singleton", and a "handler" applies to a single opened table. So a handlerton is a singleton for all the tables in a particular storage engine, the capabilities for it as the whole, although later we'll see that some of the table flags are engine-level too. Perhaps that happened because handlerton concept was introduced later, in 5.0 series AFAIK, and it was all handler earlier.

The list in the source code starts with #define and ends with constexpr, showing its evolution as the coding standards evolve. Although it all should be somehow made into an enum class bitset, I think, with a healthy part of handler flags moved over here too.

Features

  • HTON_ALTER_NOT_SUPPORTED: cannot do ALTER TABLE, i.e. performance_schema.
  • HTON_TEMPORARY_NOT_SUPPORTED: cannot do CREATE TEMPORARY TABLE, i.e. performance_schema again.
  • HTON_SUPPORT_LOG_TABLES: can host mysql.general_log and slow_log tables. CSV and MyISAM!
  • HTON_NO_PARTITION: tables cannot be partitioned. performance_schema, Warp.
  • HTON_SUPPORTS_EXTENDED_KEYS: all the secondary keys include the primary key columns in the end. This is a feature for the query optimizer. InnoDB, MyRocks, TokuDB.
  • HTON_SUPPORTS_FOREIGN_KEYS: self-descriptive. InnoDB, NDB.
  • HTON_SUPPORTS_ATOMIC_DDL: DDL statements are atomic, nice. InnoDB, NDB.
  • HTON_SUPPORTS_PACKED_KEYS: MyISAM something something.
  • HTON_SUPPORTS_SECONDARY_ENGINE, HTON_SECONDARY_ENGINES_DDL: too early to tell, the secondary engine feature is under development, and even then it might not be public, i.e. something for HeatWave.
  • HTON_SUPPORTS_TABLE_ENCRYPTION: self-descriptive. InnoDB.
  • HTON_SUPPORTS_ENGINE_ATTRIBUTE: whether CREATE and ALTER can take ENGINE_ATTRIBUTE for engine-specific attributes provided by user.
  • HTON_SUPPORTS_GENERATED_INVISIBLE_PK: will create a hidden primary key if you don't provide one. InnoDB.

Hidden Storage Engines

  • HTON_HIDDEN: do not advertise as a storage engine at all, do not allow CREATE TABLE in it etc. Binary log is such storage engine!
  • HTON_NOT_USER_SELECTABLE: do not allow user access any tables in this engine. Binlog again. Not sure why it cannot be merged with HTON_HIDDEN.

Implementation Details

  • HTON_CLOSE_CURSORS_AT_COMMIT: last in-tree engine that used it was BDB in 5.0, which I am not familiar with. MariaDB docs (here goes Planet MySQL) say this was BerkeleyDB. But also TokuDB.
  • HTON_CAN_RECREATE: map TRUNCATE TABLE to DROP & CREATE internally. MyRocks, Warp.
  • HTON_FLUSH_AFTER_RENAME: introduced somewhere between 5.0 and 5.1, removed in 8.0. An implementation detail for the BDB storage engine.
  • HTON_NO_BINLOG_ROW_OPT: if using row-based binlog format, do not do anything about before/after row images for this engine, meaning all the columns will be always included. NDB.

Handlerton Flags for Foreign Keys

Foreign key capabilities are finer-grained. Archeologically we are above the #define layer, but below the constexpr layer. These are static const, so they are emitted in every single compilation unit including handler.h, unless the compilers manage to clean it up.

  • HTON_FKS_WITH_PREFIX_PARENT_KEYS: whether the foreign key can have arbitrary columns with resulting non-uniqueness etc. as long as the unique parent key columns are in its prefix. InnoDB.
  • HTON_FKS_WITH_SUPPORTING_HASH_KEYS: the foreign key can be a hash key. NDB.
  • HTON_FKS_NEED_DIFFERENT_PARENT_AND_SUPPORTING_KEY: can't have a self-referring foreign key. InnoDB.
  • HTON_FKS_WITH_EXTENDED_PARENT_KEYS: if the keys have primary key columns in its suffix, use that. InnoDB.

Optional Handlerton Function Pointers

We are done with handlerton flags, but we are not done with handlerton. Turns out, some of its function pointers are optional, and capabilities are inferred from them being non-null. A lot of them are transaction-related. These capabilities are:

Savepoints

  • savepoint_set: transaction savepoints. InnoDB, MyRocks, TokuDB.
  • savepoint_rollback_can_release_mdl: ability to rollback MDL locks taken after a savepoint on rollback there. InnoDB, MyRocks

XA Transactions

  • commit_by_xid, rollback_by_xid, set_prepared_in_tc, set_prepared_in_tc_by_xid: XA. InnoDB, MyRocks, TokuDB.
  • recover: can recover XA transactions on startup. InnoDB, MyRocks.
  • recover_prepared_in_tc: can recover prepared XA transactions on startup. InnoDB.

Other Features

  • prepare, set_prepared_in_tc: 2PC support. InnoDB, MyRocks.
  • start_consistent_snapshot: supports START TRANSACTION WITH CONSISTENT SNAPSHOT. InnoDB, MyRocks.
  • flush_logs: self-descriptive. InnoDB, MyRocks, TokuDB.
  • show_status: Likewise.
  • alter_tablespace, get_tablespace_type_by_name: supports tablespaces. InnoDB.
  • is_supported_system_table: can host at least some of the mysql schema tables. InnoDB.
  • get_table_statistics, get_index_column_cardinality, get_tablespace_statistics, prepare_secondary_engine, optimize_secondary_engine, compare_secondary_engine_cost: self-descriptive. InnoDB, MyRocks.
  • clone_interface: can clone. InnoDB. Watch this space for more exciting announcements!
  • lock_hton_log, unlock_hton_log, collect_hton_log_info: can participate in performance_schema.LOG_STATUS table. InnoDB, MyRocks

Implementation Details

  • replace_native_transaction_in_thd, ddse_dict_init, dict_...: native data dictionary support. InnoDB.
  • page_track: can be used with the page tracking service.
  • drop_database: wants to participate in DROP DATABASE implementation. NDB.
  • panic: can be shutdown, even if it's statically compiled in.
  • fill_is_schema: wants to fill in some of INFORMATION_SCHEMA tables by itself.
  • get_tablespace: unused.
  • post_ddl, post_recover, check_fk_column_compat, se_before_commit, se_after_commit, se_before_rollback: just let's say it's all implementation details.

Handler Table Flags

These flags are per-table, but before handlerton was a thing, a lot of per-engine flags were added too.

Features

  • HA_NO_TRANSACTIONS: this is not a real OLTP database storage engine. MyISAM.
  • HA_CAN_SQL_HANDLER: supports HANDLER interface. InnoDB.
  • HA_NO_AUTO_INCREMENT: does not support auto increment fields. performance_schema, Warp.
  • HA_HAS_CHECKSUM: CREATE TABLE supports CHECKSUM option. MyISAM.
  • HA_CAN_REPAIR: can REPAIR TABLE. MyISAM, Warp.
  • HA_CAN_EXPORT: supports FLUSH TABLE FOR EXPORT. InnoDB.
  • HA_SUPPORTS_DEFAULT_EXPRESSION: DEFAULT column clauses may be expressions. InnoDB, MyRocks, MyISAM.
  • HA_UPDATE_NOT_SUPPORTED: self-descriptive. ARCHIVE.
  • HA_DELETE_NOT_SUPPORTED: likewise.

Supported Column and Index Types

  • HA_NO_PREFIX_CHAR_KEYS: does not support indexing CHAR column prefixes. NDB.
  • HA_NO_VARCHAR: does not support VARCHAR type. No engine sets this.
  • HA_CAN_FULLTEXT: supports fulltext indexes. InnoDB.
  • HA_NO_BLOBS: does not support BLOBs.
  • HA_CAN_INDEX_BLOBS: can index BLOBs. InnoDB, MyRocks, TokuDB.
  • HA_BLOB_PARTIAL_UPDATE: BLOBs can be updated partially. InnoDB.
  • HA_CAN_GEOMETRY: can handle spatial data. InnoDB.
  • HA_CAN_RTREEKEYS: supports R-Tree indexes.
  • HA_SUPPORTS_GEOGRAPHIC_GEOMETRY_COLUMN: geometry supports can do not only coordinates on a flat surface, but geographic coordinates too. InnoDB.
  • HA_CAN_BIT_FIELD: supports BIT type. MyISAM.
  • HA_GENERATED_COLUMNS: supports generated columns. InnoDB, MyRocks, MyISAM.
  • HA_CAN_INDEX_VIRTUAL_GENERATED_COLUMN: can index them. InnoDB, MyRocks.
  • HA_ANY_INDEX_MAY_BE_UNIQUE: self-descriptive. MRG_MyISAM.
  • HA_DESCENDING_INDEX: supports descending indexes. InnoDB.
  • HA_MULTI_VALUED_KEY_SUPPORT: can index JSON arrays. InnoDB.

Query Optimizer Features

  • HA_PARTIAL_COLUMN_READ: can return a subset of row columns. InnoDB, MyRocks.
  • HA_TABLE_SCAN_ON_INDEX: a clustered index always exists and should be used for table scans. InnoDB, TokuDB.
  • HA_FAST_KEY_READ: random key order is as fast as sequential. Set for in-memory engines, but probably should be set for everything on SSD?
  • HA_NULL_IN_KEY: can index NULL. InnoDB, MyRocks, TokuDB.
  • HA_STATS_RECORDS_IS_EXACT: table record count in statistics is exact. InnoDB.
  • HA_PRIMARY_KEY_IN_READ_INDEX: when doing an index-only read on a secondary key, primary key columns will be returned too. InnoDB, MyRocks, TokuDB.
  • HA_PRIMARY_KEY_REQUIRED_FOR_POSITION: to position() a handler, primary key is needed. Otherwise, position works as a row counter. InnoDB, MyRocks, TokuDB.
  • HA_COUNT_ROWS_INSTANT: not sure what the difference from HA_STATS_RECORDS_IS_EXACT is. The name suggests that counting records is instant, and the comment suggests that it's exact. MyISAM.
  • HA_READ_BEFORE_WRITE_REMOVAL: supports blind writes, i.e. to write a row you don't have to read it first. NDB. But, this has no relation to a similar optimization in MyRocks and TokuDB.
  • HA_BLOCK_CONST_TABLE: disables const-table optimization. NDB.
  • HA_CAN_FULLTEXT_HINTS: can provide extra FULLTEXT-related info in EXPLAIN SELECT? InnoDB.

Implementation Details

  • HA_UNUSED3: used to be HA_REC_NOT_IN_SEQ before 8.0. Implementation detail.
  • HA_REQUIRES_KEY_COLUMNS_FOR_DELETE: need to read all the key columns before deleting a row by that key. Unused in MySQL, used by MariaDB MyISAM and InnoDB.
  • HA_DUPLICATE_POS: on duplicate key error, provide the position of the existing key.
  • HA_AUTO_PART_KEY: multi-part keys can include auto increment columns. TokuDB.
  • HA_REQUIRE_PRIMARY_KEY: a user-provided primary key must exist. No SE uses this.
  • HA_UNUSED14: until 5.7 it was HA_CAN_INSERT_DELAYED. MyISAM.
  • HA_PRIMARY_KEY_REQUIRED_FOR_DELETE: DELETE and UPDATE work by primary key. performance_schema.
  • HA_FILE_BASED: each table is stored in a separate file. MyISAM, TokuDB.
  • HA_NO_COPY_ON_ALTER: the name suggests that "ALTER TABLE does not copy data", but this is different from online ALTER. MRG_MyISAM.
  • HA_HAS_OWN_BINLOGGING: NDB.
  • HA_BINLOG_ROW_CAPABLE, HA_BINLOG_STMT_CAPABLE: what binary log formats are supported.
  • HA_DUPLICATE_KEY_NOT_IN_ORDER: multiple key conflicts in a multiple value-REPLACE statements are reported not in the ascending key name order. Unused in MySQL, used by CONNECT storage engine in MariaDB.
  • HA_CAN_FULLTEXT_EXT: supports "extended fulltext API", i.e. it is InnoDB and not MyISAM.
  • HA_READ_OUT_OF_SYNC: the storage engine is BLACKHOLE. I cannot make more sense out of the description: "what you read will not be what is expected to be in the table".
  • HA_NO_READ_LOCAL_LOCK: does not support LOCK TABLE READ LOCAL, and does not want them to be upgraded to LOCK TABLE READ by the server. InnoDB.
  • HA_ATTACHABLE_TRX_COMPATIBLE: the storage engine is "compatible" with attachable transactions. It is a superset of "supporting" them. InnoDB and MyISAM.
  • HA_NO_INDEX_ACCESS: the indexes are not really indexes, because they don't support access through them. MOCK (that's a storage engine name, not a suggestion).

Index Flags

These are capabilities for individual indexes.

  • HA_READ_NEXT: the index can go from one record to the next. Not used, always assumed.
  • HA_READ_PREV: the index can go to the previous record.
  • HA_READ_ORDER: the index is ordered. Usually might mean not a hash index.
  • HA_READ_RANGE: the index supports ranges, again not a hash index, usually.
  • HA_ONLY_WHOLE_INDEX: the index does not support key prefix search. Hash indexes again.
  • HA_TABLE_SCAN_ON_NULL: the index does not store NULLs even if the underlying column does, forcing a switch to table scan. NDB.
  • HA_KEYREAD_ONLY: index-only scans are supported. InnoDB, MyRocks, TokuDB.
  • HA_KEY_SCAN_NOT_ROR: Not sure. Will update if I figure it out. InnoDB.
  • HA_DO_INDEX_COND_PUSHDOWN: supports Index Condition Pushdown. InnoDB, MyRocks, TokuDB.

Handler Partitioning Flags

Storage engines can implement partitioning with different levels of capabilities.

  • HA_CAN_UPDATE_PARTITION_KEY: can do UPDATE involving the partitioning key. NDB.
  • HA_CAN_PARTITION_UNIQUE: can handle unique indexes across partitions correctly. NDB.
  • HA_USE_AUTO_PARTITION: not sure what "automatic partitioning" is, but this is something NDB supports.
  • HA_CAN_EXCHANGE_PARTITION: can ALTER TABLE EXCHANGE PARTITION with a non-partitioned table. InnoDB.
  • HA_CANNOT_PARTITION_FK: a foreign key cannot be the partition key. MyRocks, TokuDB.
  • HA_TRUNCATE_PARTITION_PRECLOSE: TRUNCATE PARTITION requires closing the involved tables. InnoDB.

Writing this post resulted in three bug reports asking to remove get_tablespace, HA_REQUIRES_KEY_COLUMNS_FOR_DELETE, and HA_DUPLICATE_KEY_NOT_IN_ORDER.

Monday, November 28, 2022

Rust for a C++ Engineer

Collected from reading https://doc.rust-lang.org/book/. The notes are organized by topic rather than by the book order.

Primitive types

  • Integer types are sized, except for isize and usize, whose width is architecture-specific, and corresponds to C++ std::ssize_t/std::intptr_t and std::size_t/std::uintptr_t respectively.
  • char != u8 != i8, oh thank god
  • String literals "foo" in UTF-8. They are slices (see below).
  • Tuples are built into language, use parentheses syntax, roughly correspond to std::tuple and std::pair. Can also do .i syntax to access the i-th element. Unit tuple ().
  • Fixed-size arrays are built into language, use bracket syntax, roughly correspond to std::array with mandatory bounds-checking.
  • Simple types implement Copy trait (i.e. scalars, correspond to value types in Java) and their assignment copies. Tuples copy if all their elements copy, and this may include arbitrary large tuples. Otherwise, assignment moves. A copy is done by an explicit .clone() operation.
  • String slices have type &str, can be created with &var[x..y] syntax, x and/or y may be omitted if they are zero and length respectively. Slice consists of a pointer and length.
  • General slices are very similar. They have &[element_type] type.
  • ! is an empty type with no values. Used as return type for functions that never return. It can be coerced into any other type, which is used for i.e. match arms which e.g. continue or panic!.

Variables & type inference

  • Variables (let) are immutable by default, but their names can be shadowed, allowing sequences of changes in what a given variable name means. The type can be changed too. Mutable variables can be declared with let mut.
  • Even immutable variables may be declared without initialization.
  • Variable types are inferred, but can be annotated when/if needed.
  • Constants are declared with const, and require type annotations.
  • Function parameter and return types always must be provided.

Lifetimes, references & borrow checker

  • All variables are owned by their enclosing scope. The ownership may be passed around, when the last owner scope exits, the variable is destroyed (drop is executed if the Drop trait is implemented, memory released).
  • Passing variable into a function moves it (or copies), transferring its ownership into the function. Likewise returning it moves it (or copies), transferring the return value ownership to the caller.
  • Creating references is called borrowing. Immutable references do not allow modifying the pointed-to variable and are borrowed with &, can be passed around without transferring ownership. Several of them may be active at the same time. Mutable references are borrowed with &mut, and no other mutable or immutable references may exist at the same time.
  • Each reference has a lifetime. Most cases are handled by implicit lifetimes. Lifetimes are named by 'a, usually very short names, placed after &.
  • For function signatures, generic lifetime parameters use angle brackets:
fn foo<'a>(x: &'a str, y: &'a str) -> &'a str
  • Lifetime annotations in struct definitions limit struct lifetime to that of its fields.
  • If there are multiple input lifetime parameters, but one of them is &self or &mut self, its lifetime is assigned to all output lifetime parameters.
  • Deref coercion converts a reference to a Deref-implementing type to a reference to a different type. I.e. &String to &str. Happens automatically on parameter-argument type mismatch: from &T to &U when T: Deref<Target=U>. Internally as many .deref() calls are inserted as needed.
  • For mutable references, implement DerefMut trait. Two extra deref coercion rules: from &mut T to &mut U when T: DerefMut<Target=U> and from &mut T to &U when T: Deref<Target=U>.
  • The Drop trait is the closest thing to a C++ destructor, adds a drop method that takes a mutable reference to self.
  • For structs, field lifetimes are part of the struct type, and should be specified where the struct type is specified.
  • 'static lifetime is the lifetime of the whole program.
  • Lifetimes are a type of generics, so a function with both lifetimes and generic type parameters lists both together in angle brackets.

On paper Rust lifetimes appear to be a genius idea. Besides manual resource management and GC, this is a viable third option that combines the advantages of the two while avoiding their disadvantages, althought only partially so.

Google Chrome developers C++ tried this and did not succeed enough for it to be viable: Borrowing Trouble: The Difficulties of a C++ Borrow-Checker.

Statements & expressions

  • Last nonterminal symbol in a block may be expression (lacking the final semicolon), in which case the whole block is an expression with this return value. This makes return expression; in functions replaceable with expression. This also merges if statement and ternary operator to a single if expression.
  • loop starts an infinite loop. break may return an expression, making the loop an expression. Nested loops may have labels 'label: loop {, then possible to do break 'label;.
  • for is a range loop. When possible, for loops seem to be more idiomatic than while loops.

Type aliases, structs & enums

  • Rust type alias type Foo = ExistingType is like C++ using Foo = ExistingType. Can be generic: type Foo<T> = std::result::Result<T, std::io::Error>.
  • Structures use struct keyword, contain only type-annotated fields.
  • If during a struct variable construction a field and a var it is initialized from have the same name, one of the can be omitted (field init shorthand).
  • ..var in the struct variable construction takes all the unspecified fields from var of the same struct type.
  • Tuple structs struct foo(i64, i64, i64) are structs that are very similar to tuples. Fields are unnamed.
  • Unit structs struct Foo;
  • If structs need to store references, then lifetimes have to be used, that's for later.
  • Attribute #[derive(Debug)] for struct allows doing {:?} in println! to dump the fields.
  • dbg!(value) macro maybe inserted as an expression to dump value
  • struct may have methods attached to them, in separate impl StructName blocks.
  • The first arg of a method may be one of
    • &self, corresponds to a C++ const method;
    • &mut self, corresponds to a regular C++ method;
    • self, consumes the object;
  • A function in an impl StructName not taking a self is an associated function, not a method, corresponding a C++ static method. Called through StructName:: syntax.
  • There may be muliple impl blocks.
  • The simplest Rust enum roughly matches C++ enum class. But then each enum variant may have different associated data with it, making it similar to std::variant
  • Standard library Option enum handles the use cases for nullptr (a null reference does not exist in Rust). Similar to C++ std::optional.

Closures & function pointers

  • Closures: || with parameters inside followed by an (optionally bracketed) expression. Parameter and return types are not annotated usually.
  • Once closure types are inferred, they don't change, cannot call the same closure with different ones.
  • Variables are captured by different types of borrowing / ownership taking implicitly depending on what the code does. move keyword before || forces taking ownership, when the body does not need it implicitly. One use case is passing data to a new thread.
  • All closures implement FnOnce trait, meaning they can be called once.
  • Closures that mutate captured values but don't move them out implement FnOnce and FnMut.
  • Closures that don't mutate captured values and don't move them out implement FnOnce, FnMut, and Fn.
  • All functions coerce to the fn type, which is the function pointer type. It implements all of FnOnce, FnMut, Fn.
  • To return a closure, use a trait object, e.g. -> Box<dyn Fn ... >

Generics & traits

  • Generics (types) and traits (behavior) resemble C++ templates. Traits also resemble interfaces in other languages.
  • Generic type uses must be constrained by traits–no SFINAE. C++ concepts.
  • impl Foo<f32> {...} adds implementation for a specific type, similar to C++ template specialization.
  • Separate traits are implemented for structs in separate blocks: impl Trait for Struct { ... }
  • Traits need to be brought into scope too, pulling in an implementing type is not sufficient to call trait methods.
  • Can implement a local trait on an external type or an external trait on an local type, but not an external trait on an external type (so no C++ std::hash specialization for a std:: type). This is to avoid allowing multiple trait implementations for the same type.
  • Trait methods may have default implementations, which may call other, possibly unimplemented methods in the same trait. The default implementation may not be called from an overriding implementation.
  • Trait-type parameters without generics syntax: fn foo(bar: &impl Trait).
  • Trait bounds, using generics syntax: fn foo<T: Trait>(bar: &T). Same as above.
  • Multiple trait bounds: fn foo(bar: &(impl Trait1 + Trait2)) and fn foo<T: Trait1 + Trait2>(bar: &T)
  • In the case trait bounds become long, where clauses pull them aside:
fn foo<T, U>(t: &T, u: &U) -> Result
where
    T: Trait1 + Trait2,
    U: Trait1 + Trait3,
{
    ...
}
  • Can use fn ... -> impl Trait to return a trait-implementing type, as long as it's a single type.
  • Can conditionally implement methods for generic structs by adding trait bounds to their implementation: impl<T: Trait> Type<T> { ... }. These are called blanket implementations.
  • Rust does not have OOP inheritance. Some form is available through default trait method implementations. Dynamic dispatch (C++ virtual methods) is through trait objects.
  • A trait object is pointer to an instance of a type and a pointer to a vtable.
  • Struct and enum vars in Rust are not objects, trait objects come close, but they cannot contain data.
  • Must be a reference (or a smart pointer) to dyn trait type, i.e. Box<dyn Trait>.
  • Associated types. type Name allows to use Name as a type in a trait before its declaration is given by the trait implementors. In C++ one would use template argument dependent typenames.
  • Default generic type parameters: <T=DefaultType>.
  • foo.bar() can be replaced by Type::bar(&foo) when bar is implemented by more than trait to disambiguate. If even more disambiguation is needed, i.e. for associated methods without self parameter, <Type as Trait>::bar calls the method from Trait as implemented for Type.
  • If a trait depends on another trait, the latter is called a supertrait: trait Foo: SuperTraitBar.
  • Newtype pattern. One use case: implement external traits on external types, declare a new thin wrapper tuple struct. There are other use cases.

Error handling

  • panic! macro exits (or aborts, depending on config) on unrecoverable error.
  • Errors are handled using Result enum, which can be Ok or Err.
  • unwrap returns the success variant of Result or panics.
  • unwrap_or_else executes given code instead of panicking.
  • expect is like unwrap with a given error message for panicking.
  • ? operator after a call, e.g. let foo = bar()?; unwraps returned Result, or returns from the caller with the error. If the error types do not match, From trait converts.
  • ? operator works with Option return types too.

Iterators

  • Rust iterators correspond to C++ ranges (or iterator pairs).
  • Calling .iter() on a collection roughly corresponds to a C++ .cbegin(), except that the latter is not a range. Other options are .into_iter() to take ownership of values–not sure what a direct C++ mapping would be–and .iter_mut() over mutable references (C++ .begin()).
  • Iterators implement the Iterator trait.
  • Iterator .collect() method returns a new collection from iterating.
  • Code using iterator adapters might be faster than equivalent loop-based code. An example of Rust zero-cost abstractions, which of course is found in C++ as well.

Pattern matching & related control flow

  • Pattern matching can decompose a tuple to local vars, and do many other things.
  • match is a generalized switch with pattern matching, variable binding, and more.
  • _ is a catch-all non-binding pattern, like the default in switch. Ignores the entire value.
  • if let behaves like a single match arm, combining if with variable binding in the case of true condition.
  • while let loop repeats until its pattern matches.
  • The value after for keyword in a for loop is a pattern.
  • let keyword takes a pattern, not a variable id.
  • Function parameters are patterns.
  • Patterns are refutable and irrefutable, the latter ones matching any possible passed value. Function parameters, let, and for take irrefutable patterns. if let and while let take both kinds, with a compiler warning if irrefutable (as that creates always-true if or an infinite loop while). match arms must be refutable except for the last one, which should be irrefutable (if the possibilities were not exhausted until then).
  • Multiple patterns can be combined with |.
  • An inclusive range of values can be matched with ..=. The range cannot be empty.
  • Struct destructuring: Foo { x: a, y: b } = v gets a and b. If field and var names match, then Foo { x, y } = v. Literals can be used too.
  • Enum destructuring Foo::Variant { x, y }, Foo::VariantWithNoData.
  • Can destructure arbitrarily deep nested structs and enums.
  • Nested _ ignores just that part.
  • Starting a variable name with an _ suppressed unused variable warnings for it.
  • .. is a greedy sequence of _, i.e.
let numbers = (1, 5, 7, 20, 30);
match numbers {
    (first, .., last) => ...
}
  • match arms may have match guards which are extra if conditions that can use the bound vars. Some(x) if x > 5. Exhaustiveness is not checked.
  • @ bindings allow to create a var holding the tested value at the match time, i.e. Message::Hello { id: id_var @ 3..=7 }.

Standard library types

  • Dynamic strings: String, would correspond to std::string type, but UTF-8. Display trait adds to_string method. Not indexable to avoid byte/UTF-8 encoding mixup. Slicing is allowed but runtime-checked to fall on char boundary. To disambiguate byte/char interpretation, use .chars() or .bytes().
  • Standard library vectors match std::vector. A macro vec![1, 2, 3] to create a vector with given contents. Ownership/borrowing rules apply to whole vector, i.e. if a mutable reference to the first element is taken, a new one cannot be pushed to the back.
  • Standard library hash maps correspond to std::unordered_map.
  • std::thread::spawn(closure) -> std::thread::thread(callable).

Smart pointers & dynamically-sized (unsized) types

  • Smart pointers own data. They implement Deref and Drop traits. String and Vec<T> are smart pointers.
  • Box<T> is like std::unique_ptr<T> in C++, except that Rust is more likely to use plain references and lifetimes, so no 1:1 mapping in i.e. rewrite. Box::new is std::make_unique.
  • Implementing Deref trait enables dereferencing with the * operator, like overloading C++ * and -> operators does.
  • Under the hood *x is transformed to *(x.deref()) exactly once.
  • std::mem::drop corresponds to C++ std::unique_ptr::reset or other early destruction.
  • Rc<T> matches std::shared_ptr. Rc::clone method matches std::shared_ptr copy constructor.
  • Interior mutability pattern: unsafe code to mutate data inside an immutable value even with immutable references present.
  • RefCell<T> does borrow checking at runtime instead of compile time. borrow and borrow_mut methods.
  • Rc<RefCell<T>> pattern implements multiple owners to potentially-mutable data.
  • Weak<T> matches std::weak_ptr. Constructed by Rc::downgrade. Upgraded by upgrade method, corresponding to std::weak_ptr::lock.
  • Dynamically sized types (DST) or unsized types, whose sizes are only known at the runtime. Cannot create variables of such types directly, naturally always hidden in some pointer + size structure. Rust automatically implements Sized trait for every non-DST, and implicitly bounds by it for every generic function. To relax the latter, fn foo<T: ?Sized>.... The ?Trait syntax is only available for Sized trait.

Operator overloading

  • Operator overloading by implementing the desired traits in std::ops.

Concurrency

  • thread::spawn returns a JoinHandle, which has a join method, similar to C++ std::thread::join.
  • Message passing for inter-thread communication, like in Go. Channels, std::sync::mpsc::channel(). The endpoints have send, recv, try_recv methods. The receiver implements Iterator too. The channels may have multiple transmitters (it's MPSC), which can be created by .clone.
  • Messages must implement Send trait. If a type is composed of Send types only, it becomes Send automatically.
  • Types whose variables are safe to be referenced from multiple threads implement Sync trait. If &T is Send, then T is Sync. A type made of Sync types only is Sync automatically.
  • Mutex<T> is a mutex-guarded variable of T. .lock() returns a LockResult, which has a (potentially mutable) reference MutexGuard to the guarded data. The guard unlocks when it goes out of scope. Mutex implements interior mutability.
  • Arc<T> corresponds to C++ std::atomic<std::shared_ptr>.
  • To actually share mutexes between threads, wrap them: Arc<Mutex<T>>.

Assorted standard library functionality

  • std::env::args is for int argc, char *argv[]. It's Unicode-checking, if that hurts then std::env::args_os.
  • std::process::exit is for exit
  • std::env::var is for getenv
  • println! prints to stdout, eprintln! to stderr.

Build and dependency management

  • cargo seems to be a much better story than CMake hell or its alternatives.
  • A crate is the smallest compilation unit, either a library crate, or a binary crate. Usually means the former. A crate root is the starting source file in it. A package is a bundle of crates with at most one library crate. Standard paths inside a package: src/main.rs, src/lib.rs, src/bin.
  • Release profiles correspond to a mixture of CMake build configurations, NDEBUG define, etc. in C++. dev profile corresponds to Debug, and release~to ~Release (or RelWithDebInfo?).
  • Can customize profiles in Cargo.toml, i.e. optimization levels.
  • Workspaces organize related packages together in large projects, to share directory root, Cargo.toml, and Cargo.lock.

Modules

Not familiar enough with C++ modules to compare.

  • Modules (and submodules) inside a crate do namespaceing and public/private. src/modulename.rs, src/modulename/submodulename.rs. Modules can be private or public, declared with pub mod and mod.
  • super:: as a part of name path goes one level up.
  • use keyword imports. Idiomatically functions are imported through their parent module, everything else directly.
  • use ... as ... creates name alias.
  • pub use re-exports. Used to organize and collect public API from several potentially nested submodules.
  • Nested path syntax: use foo::{bar, baz, self};, globs use foo::*;

Tooling

  • rustfmt formats, so does clang-format.
  • Clippy the linter.
  • rust-analyzer for LSP support.

Documentation

  • Documentation header comments start with /// and support Markdown. Built by cargo doc [--open].
  • Typical API doc sections: Examples, Panics, Errors, Safety.
  • Contained documentation comments start with //!, typically used for crates and modules.

Testing & benchmarking

  • #[test] annotates a function to be a test, i.e. Google Test TEST macro in C++. Tests run in parallel by default.
  • assert_eq! is like gtest ASSERT_EQ, except that the args are 'left' and 'right' instead of 'expected' and 'actual' or similar. Equality asserts may be applied on types implementing PartialEq and Debug traits.
  • assert! may take 2nd and subsequent args for a message in the case of failure.
  • Tests annotated with #[should_panic] test that the annotated function panics, similar but not identical to gtest death tests. Best to add expected parameter to the attribute to specify the reason for panic.
  • Tests may also be implemented by returning a Result<T, E>.
  • Benchmark tests correspond to Google Benchmark, but unstable ATM.
  • Documentation tests can compile API examples automatically.
  • Unit tests go with the code they test, mod tests annotated with #[cfg(test)]
  • Visibility rules happen to allow the testing of private functions.
  • Integration tests go to a top-level tests directory, no configuration annotation. Each file there is a separate (test) crate–if that's not what's needed, i.e. for setup code, use foo/mod.rs naming convention for non-tests.
  • cargo test runs in sequence: unit, integration, doc, does not go to the next category if failure.
  • Binary crates cannot have integration tests directly. The usual thing to do is to always have a library crate with a binary crate as minimal as possible.

Macros

  • There are macros, names trailing with exclamation mark (println!).
  • Macros can take Rust code and expand to a different Rust code. A difference from C++ preprocessor that it works on the AST, not textually. While powerful, how well does this work with tooling? Do they run macros? Can they refactor macros?
  • Declarative macros (macro_rules!) pattern-match given code to produce code.
  • #[macro_export] annotation for public macros.
  • Procedural macros take token stream input and produce token stream output.
  • One kind is custom derive macros that add code to a struct implementation.
  • Attribute-like macros allow creating new attributes.
  • Function-like macros are close to C preprocessor function-like macros, except that they also operate on TokenStream and not on arguments directly. Can take variable number of arguments.

Unsafe Rust & FFI

  • unsafe { ... }: allows some, well, unsafe features
  • unsafe can dereference raw pointers *const T, *mut T. Raw pointers are just like C raw pointers.
  • unsafe fn foo() {}, then fn can be called from unsafe code.
  • extern "C" { fn putenv ... } for FFI, may only be called from unsafe code.
  • To make Rust function callable from external code, add #[no_mangle] annotation and pub extern "C" before the fn.
  • Static variables may be declared with static FOO_BAR: type = value; Immutable static vars have an address in memory; constants don't. All mutable static vars are unsafe.
  • unsafe trait Foo, unsafe impl Foo for Bar.
  • union types exist, mainly used for interfacing with C unions, accessing fields is unsafe.
  • Raw identifier syntax r#while allows using e.g. a keyword for an identifier. Useful for FFI and different Rust edition interfacing.