Embedded Systems

Building maintainable command-line tools with MRuby

Building maintainable command-line tools with MRuby

by Eric Hodel

In this presentation, Eric Hodel discusses Building maintainable command-line tools with MRuby at RubyConf 2016. The talk explores the MRuby language and mruby-cli, detailing their capabilities for creating single binary command-line tools. Hodel begins by providing his background, emphasizing his experiences with Ruby and open-source contributions, particularly through Fastly, which supports Ruby projects. He explains the benefits of MRuby over CRuby, such as its lightweight nature, ease of embedding into applications, and the ability to create standalone executables without requiring users to install Ruby or libraries.

Key points of the presentation include:

- MRuby vs. CRuby: MRuby allows for a lightweight embedding of Ruby for low-resource environments, supporting a limited standard library and additional gems.
- Development Patterns: Unlike CRuby, MRuby requires advance specification of dependencies in the build configuration, which Hodel discusses through examples of building tools and libraries.
- Challenges: He notes challenges in transitioning from CRuby to MRuby, including the restricted set of methods in MRuby's core classes and the complexity involved in cross-compiling C libraries.
- Docker Integration: Hodel describes how Docker is used for the MRuby CLI build process, mentioning both the advantages of cross-compiling and the difficulties in debugging and executing commands.
- Testing Strategies: Significant time is devoted to discussing testing within the MRuby context, highlighting the MRuby Test gem and complexities involving setup and execution of tests across platforms.

Throughout the talk, Eric illustrates these points with personal anecdotes related to building customer tools for Fastly and reflects on the software development process's emphasis on documentation and maintainability. He concludes by offering suggestions for potential improvements to MRuby and its ecosystem, including enhanced documentation, better debugging tools, and the introduction of keyword arguments in MRuby's C API, which would aid in more robust argument verification. Overall, Hodel conveys excitement about the possibilities that MRuby brings to command-line tool development while candidly sharing the hurdles encountered along the way.

00:00:14.760 Hello and welcome to the talk on building maintainable command-line tools with MRuby. I am Eric Hodel, and this presentation will focus on working with MRuby and mruby-cli.
00:00:22.540 A bit about me: I am a Ruby committer, a rubygems committer, and the author of many gems. Currently, I’m on vacation from most of my open-source work. Instead of working on Ruby in my spare time, I have been spending a lot of time making shelves, like the one shown on the screen.
00:00:35.020 The far shelf is still a work in progress; I’m trying to smooth out some gaps in it. I work for Fastly, a real-time content delivery network, and this is our amazing website, fastly.com, featuring Gordo, one of the dogs at Fastly.
00:00:46.180 Fastly supports many open-source projects, including hosting the Ruby language and providing downloads for RubyGems. Last month, Fastly served 425 terabytes of data and handled eight billion requests, supporting RubyGems for free. I’d also like to highlight the work of David Radcliffe and the rest of the rubygems.org team for speeding up gem downloads, especially for users outside the United States. They will be making a blog post about that work soon.
00:01:11.170 You can follow Fastly on Twitter. Now, moving on to part one: my exploration of MRuby and mruby-cli. We’re building a new product at Fastly with new APIs, and I wanted an easy way to interact with those APIs to demonstrate their usability.
00:01:30.130 Why not choose Ruby to write the wrapper? When using CRuby, developers must install the correct version depending on which Ruby features may be needed. They also have to install the gems they depend on and keep both the Ruby version and the gems up to date for the duration of tool support.
00:01:44.590 MRuby is very similar to CRuby, so we can start with a prototype in CRuby, port it to MRuby, and then handle maintenance in MRuby. But what makes MRuby more suitable than CRuby? MRuby is a lightweight Ruby implementation; you only use the features of Ruby that you need, which keeps the language size smaller.
00:02:06.250 It is designed for embedding; you can run Ruby on low-resource hardware or embed it into other software without the hassles of embedding CRuby. MRuby has libraries, and it calls them gems. It ships with a limited standard library, so you can use a gem for an array or a gem to access the full features of CRuby.
00:02:30.700 There are also standalone MRuby C libraries, some of which are C extensions that wrap libraries, like the MRuby curl gem. You can find many talks on MRuby, including one titled 'Making High Tech Seat in MRuby,' which is about building a robot with Lego Mindstorms.
00:02:43.310 You can access recordings of these talks on the RubyKagi website. This year’s RubyConf features two talks on MRuby; one is in Ballroom A after lunch on evaluating Ruby without Ruby, and the other is the first session tomorrow about MRuby on small devices.
00:02:57.830 Since MRuby is compiled, unlike CRuby, how do you use it? The first step is to edit your buildconfig.rb. Unlike CRuby, where you can use any gem at any time, in MRuby you must specify your gem dependencies in advance in the build configuration.
00:03:09.579 You choose the compiler to use. MRuby CLI uses this for cross-compiling to different platforms. You can also specify library include paths in case you need to link with other C libraries.
00:03:25.650 If you're writing an MRuby gem, instead of embedding MRuby, you'll fill out an MRuby gem specification. Some MRuby CLI applications use the MRuby gem specification to specify the executable name and test files. The MRuby gem rake file, like RubyGems gem specifications, contains gem metadata, dependencies, and a list of binaries.
00:03:47.830 Metadata includes information like name, version, license, test files, and so on. You can also specify the gems you depend on and the binaries that get created. After you've configured these files, you run rake to build MRuby and all the executables you need. Once rake is complete, you'll see several executables in the bin directory.
00:04:07.340 If you are using MRuby CLI, the name of the command-line tool will appear in the view specified in the gem's specification. You will also have MRuby and mrb executables that have all the dependencies compiled in, which you can use to run one-off tests. If you enabled testing for this build, you'll have an MRuby test.
00:04:34.240 The goal of every MRuby CLI is to allow Rubyists to build single-file executables in a familiar environment. Last year, Terrence Lee and Zachary Scott gave a talk on MRuby CLI which showed the basics of the tool and how it works. The very basic description of MRuby CLI is that you can write Ruby and release software on Linux, OS X, and Windows.
00:05:01.310 The typical development loop using MRuby CLI begins with writing some MRuby code, compiling it, running tests, and returning to the start if the tests don't pass. Once you have enough features built and tests passing, you can release the executables.
00:05:19.900 When I first heard about MRuby and MRuby CLI, I thought it sounded easy, but I was certain there would be challenges when using a different Ruby implementation. The first challenge is that MRuby does not support everything CRuby does. For instance, MRuby's core library contains classes like String, Array, and Hash, but only supports a minimum number of methods compared to CRuby.
00:05:58.480 To access the full experience of CRuby, you need to add extension gems like MRuby Array Ext, which can be easily overlooked if you are developing your own MRuby gem. Forgetting to include these extension gem dependencies will lead to failures if the gem is used in another project.
00:06:21.700 Another issue is that MRuby has a smaller language outside the standard library; there are no predefined globals, such as load path, since everything is compiled in. Some libraries you're importing from CRuby may depend on these globals, which you would have to work around.
00:06:44.160 I encountered a problem while attempting to support documentation when porting a library from CRuby. I haven't reproduced it on its own yet, so a bug needs to be filed on it. While keyword arguments can be used to call methods in MRuby, you cannot define methods that automatically check your arguments like you can in CRuby.
00:07:08.360 If you haven't explored keyword arguments yet, there is a talk today in the second session after lunch in Ballroom C.
00:07:16.220 Also, some of the Gsub replacements don't work in MRuby, and regular expressions in MRuby seem less useful than in CRuby. I have found myself exploring the C source more to understand exceptions being raised, though I am not sure if this is due to unfamiliarity with MRuby or other reasons.
00:07:40.520 There are many pure Ruby libraries already written in CRuby that you might want to use in your MRuby projects. Overall, this process is straightforward, provided you remain aware of the restrictive syntax. Unfortunately, porting the tests for these libraries is much harder due to differences in how tests run, which we will cover later in this talk.
00:08:07.160 The existing gems for MRuby feel reminiscent of working with libraries from the Ruby 1.6 days. This could be because there haven't been enough contributors to develop libraries that are broadly applicable to different types of problems. As a result, an MRuby library often has a very narrow focus, which means you'll either have to hunt extensively to find a library that fits your use case or submit patches to get it working.
00:08:33.080 Personally, I have submitted patches for various libraries, with most being fairly small, which is a benefit. The last challenge I'll cover regarding MRuby involves extracting build information. Since MRuby is compiled, it needs to link with C libraries to compile successfully. MRuby CLI also requires cross-compiling these libraries for various platforms.
00:09:07.440 Unfortunately, extracting information from the build's MRuby build system is challenging without loading all of its rake files, which can be slow. I think enhancing this aspect could make MRuby's build system more flexible.
00:09:25.260 Working with MRuby CLI was a new experience for me, and it took time to learn the right way to use it. One of the first challenges was Docker. The MRuby CLI's build system is based on Docker, which offers the advantage of being ready to cross-compile for various systems without extra setup.
00:09:38.560 Unfortunately, Docker can be somewhat clumsy in how MRuby CLI operates; typically, Docker is used for long-lived services, while MRuby CLI only needs the container to run long enough to finish compiling or running tests. While Docker is arguably the best option available due to the minimal setup required for building these command-line tools, starting a build requires a lengthy command.
00:10:03.860 In situations where a compile problem arises, debugging can prove difficult because you have to first start a shell inside Docker, determine which commands were executed, and then run those commands to reproduce the issue before you can address it. Additionally, when you are in the Docker shell, your familiar development environment is not available, making it more challenging to perform fixes.
00:10:24.640 The most significant challenge I faced with MRuby CLI was its build system. Most of the issues I encountered stemmed from how the tasks were organized. The tasks for building your executable and those for building MRuby were all mixed together into a single rake command.
00:10:45.330 This made it challenging to add your tasks and have them run appropriately. For instance, before compiling the MRuby curl extension, the curl C library must be installed for every platform you are cross-compiling to, which is quite difficult by default.
00:11:10.680 Due to these complications, I made some improvements to the build tasks. The first was to build MRuby separately from the other work performed inside Docker, allowing me to cross-compile C libraries in advance.
00:11:22.830 The other improvement was to have a rake test that exists outside Docker, initiating Docker for me. This enhanced my capability to perform additional tasks that didn't require Docker, simplifying debugging.
00:11:38.820 The new build system runs rake three times. First, the outer rake tasks are executed, and once they are finished, the inner rake tasks run inside Docker. These inner tasks handle operations like cross-compilation or other setups that can only occur inside Docker.
00:12:00.100 Once the inner tasks are finished, MRuby's rake tasks build your command-line tool. Since the outer tasks don’t require Docker, you can utilize them for actions such as downloading or unpacking MRuby in preparation for compilation.
00:12:25.290 In addition, there are global test setup tasks that I will cover later, and finally, release tasks to publish your finished gem executable.
00:12:47.350 All the inner tasks run inside Docker, cross-compiling the C libraries you configured and subsequently invoking rake again to compile MRuby and conduct tests.
00:13:06.970 There are several additional benefits of my changes to the build system; it's now much easier to add hooks for custom tasks since tasks are well-defined for different phases of the build.
00:13:25.300 I also improved rake test performance by only compiling for the host platform when running tests, eliminating the need to build for all platforms when tests run only on the host platform.
00:13:40.100 It’s also quite likely that you will need to use C libraries as dependencies in your MRuby gem project. There are two options: you can statically link the libraries, embedding them inside your command-line tool and keeping a single file executable.
00:13:56.000 This simplifies installation since there are no additional instructions needed for users to install the relevant C libraries. However, the downside is that you will need to re-release your command if vulnerabilities in the C libraries arise, and this approach does increase the executable size.
00:14:14.640 The second approach is to dynamically link your C libraries. This has the advantage of easier vulnerability management, as the user is responsible for upgrading their libraries for any security concerns, resulting in less for you to manage.
00:14:34.230 A disadvantage of this option is that it adds an external dependency, making your executable no longer a single file. Another challenge I faced with MRuby CLI involved cross-compiling libraries.
00:14:52.480 MRuby CLI can cross-compile your executable for up to six different platforms, meaning the C libraries must also be compiled for all those platforms.
00:15:00.480 For example, for libcurl, you can't simply install the libcurl development package inside Docker and be done, as that library doesn't work on OS X or Windows.
00:15:24.970 Instead, you need to download the source, unpack it, cross-compile for each platform you'll be releasing on, and then configure MRuby's compiler to use those libraries.
00:15:47.190 To streamline this process, I wrote a tool for automating cross-compiling dependencies. You can configure the library to cross-compile by specifying a few values such as the name of the library, the release name for creating the source directory, and the URL from which the source will be downloaded. You can also set extra configuration flags.
00:16:06.300 In part two, I will discuss the origins and the structure of the command-line tool. My exploration of MRuby CLI began with a tool that our customer support team could use to configure a new product we are developing.
00:16:26.590 Our service was, and still is, API-only, and there is no web UI currently available for users. The service has a complicated setup, so I sought to automate common use cases to make it easier and reduce the potential for errors.
00:16:39.970 Moreover, I wanted to create a tool that customer support representatives could send to users, ultimately reducing their burden in supporting this new product.
00:16:59.230 To solve these challenges, I decided to write a tool that would automate setup, validate inputs, and minimize the number of errors users would encounter. It would also have the added benefit of providing design feedback on the API, allowing for further improvements before full exploration.
00:17:15.500 Before exploring MRuby CLI, I started with prototypes in CRuby. The first was a single-file script primarily serving as documentation. It was easy for me to write and allow customer support to modify it for their needs, but it wasn't flexible enough to handle all the ways they needed to interact with the API.
00:17:38.830 As customer support's needs grew, I split this single script into multiple scripts, each with its specific purpose—such as listing options, performing initial setups, or enabling/disabling features.
00:17:55.780 I focused on ensuring these scripts were well-documented to facilitate modifications for future scripts. Eventually, I extracted a couple of classes from these scripts for reuse, including a basic implementation of a JSON API and one for argument parsing using Ruby's option parser.
00:18:11.990 What lessons did I learn from these prototypes? First, documentation for the tool was extremely important. Since we had not budgeted any time for API documentation of the service, having documentation within the command-line tool proved very useful.
00:18:29.590 Roughly half the lines in the scripts ended up being comments, which allowed our support staff to understand what the tool was doing and suggest improvements beneficial to their workflow.
00:18:43.500 Keeping the scripts simple made everything more manageable. None of the scripts had external dependencies, allowing easy installation simply by checking out their repository. Additionally, most scripts were small, focusing on a single, well-defined purpose.
00:19:03.240 It was perfectly fine to have one or two larger scripts that automated key actions, such as initial setup. However, ultimately, these scripts became hard to maintain.
00:19:33.420 Our development environment relied on an older version of Ruby that did not support keyword arguments by default, necessitating the installation of a more modern version separately. Meanwhile, customer support would often add or edit existing tools to make their lives easier, resulting in multiple independent versions that were hard to support.
00:19:49.570 This led me to investigate a switch to MRuby CLI to alleviate the maintenance burden while striving to maintain high standards of maintainability.
00:20:05.360 I decided to model my command-line tool after the Ruby Gems pattern, utilizing a set of subcommands with a unique class for each while both leveraging option parser for argument parsing. Each subcommand features a common setup method to pick options for each command.
00:20:25.250 After parsing the arguments, the execute method runs the command and manages any errors that arise. For example, we have a command that lists conditions, requiring a service and version against which the conditions are associated.
00:20:50.620 The execute method then performs the necessary API operations to fetch the conditions from the service and display them. I decided to leverage a Ruby table-based approach from the CRuby standard library for option parsing.
00:21:07.220 I chose this for its familiarity among Rubyists and its good documentation. The option parser provides automatic type checking and argument completion, reducing the amount of code required to check these manually.
00:21:20.180 Moreover, it also offers shell completion by default, making parsing personal arguments easy. For API requests, I chose the MRuby curl library after evaluating several other HTTP libraries available for MRuby.
00:21:32.960 The MRuby curl library appears to be the most mature, primarily because it utilizes curl. It supports both HTTP, which we use in our development environment, and HTTPS for accessing production systems.
00:21:56.300 The library also supports persistent connections, which is especially important for HTTP requests. However, the downside is that, as a library dependency, it must be cross-compiled and linked into the tool.
00:22:25.640 There are two MRuby gems that have become quite popular: the MRuby Test gem for assertions and the MRuby Test gem used for running test cases. While using these gems is relatively simple, setting up the test cases can be a frustrating experience.
00:22:44.080 Testing presented me with the most challenges while building the command-line tool. It is crucial to writing maintainable software, but the unfamiliar environment required specific adjustments to understand testing within the MRuby CLI.
00:23:20.250 The MRuby Test gem is similar to the MiniTest library that ships with CRuby. You create a subclass of the test class and write test methods just as you would with MiniTest. Setup and teardown methods are used for common test setups.
00:23:37.700 However, you need to start tests using MRUBY_TEST_UNIT at the bottom of your test case, as MRuby lacks an exit method without an additional gem. Since MRuby is compiled, you cannot directly run the tests; instead, you rely on the MRuby Test gem to provide the test runner.
00:24:04.520 When tests run, a new MRuby VM is created for each test file, ensuring complete isolation between tests. The design of the test runner introduces complications. First, creating a global test setup requires setting test preload in your MRuby gem specification.
00:24:23.100 Without setting this, the helper won't be available to your tests since MRuby considers it a separate test file. I'm still searching for a solution to get this to work since I only discovered it recently.
00:24:42.140 The second complication involves Docker. MRuby CLI uses Docker Compose for test setups, but most documentation focuses on the Docker command, which caused some confusion in figuring out how to set up DNS records for tests.
00:25:05.300 Ideally, I would have preferred to mock interactions, but due to the lack of a straightforward test helper and mocking library, I opted to use the development services directly.
00:25:25.860 Additionally, the MRuby CLI's initial start-up is slow due to compiling all platforms beforehand, which significantly lengthens the feedback loop even for a minor change.
00:25:46.630 In response, I chose to perform some test setup before beginning any tests, utilizing the improved build structure to set everything up outside of Docker.
00:26:07.290 Given that I learned about test preload recently, I currently pass needed data for global setup into the tests using environment variables so that each test can retrieve them when needed.
00:26:27.250 Furthermore, I refined the rake tasks to only compile for the host platform used during tests, meaning I only had to compile a single platform instead of all seven.
00:26:48.750 MRuby CLI also supports bin tests, acting as integration tests, but since I prefer strong unit tests, I have yet to fully explore these.
00:27:05.740 In an integration test, you run your command and capture the output to validate its format. Despite these challenges, the question arises: why choose MRuby CLI over other tools like Go that offer similar capabilities?
00:27:29.460 I am primarily a Rubyist and work with a team of Rubyists. While we are open to learning new things, Ruby keeps us in our comfort zone. When I faced difficulties with the test setup, I could pivot back to CRuby, which was much more familiar for getting tasks done.
00:27:54.210 As I also want a tool I can deliver to Fastly customers, being able to work within my area of expertise simplifies the learning curve and reduces my stress, enhancing my ability to deliver quality software.
00:28:19.630 Now we approach the conclusion: what improvements would I like to see to make MRuby and MRuby CLI easier to use?
00:28:39.860 For MRuby CLI, I plan to contribute the improvements I made to the build system. I have had preliminary discussions with the maintainer about these enhancements and the benefits they could bring, and so far, they seem positive.
00:29:00.550 Additionally, the documentation for MRuby CLI could benefit from some improvements as it is not immediately clear where to start with a new command-line tool or what complications might arise.
00:29:25.290 There are libraries worth checking out to facilitate development, and I would also like to streamline the process for upgrading to newer versions of MRuby CLI, though I am not yet certain how that will work.
00:29:41.440 Having more accurate backtraces would be extremely helpful; currently, it's important to pay attention to pinpoint where MRuby indicated an error occurred. Moreover, I want to separate the loading of the build configuration from generating rake tasks, making it easier to unify MRuby's build system with MRuby CLI.
00:30:15.870 Lastly, it would be beneficial to have keyword arguments in methods for MRuby's C API as a fancy method of argument checking. I'm unsure how this will function with keyword arguments in CRuby, but I would like to see the mrb_get_args functionality ported over.
00:30:31.813 This function in the C API extracts method arguments when a Ruby method invokes a C function in MRuby, allowing for specifying argument types for automatic type checks and conversions.
00:30:55.250 The equivalent function in CRuby is rb_scan_args, but it is less expressive and only performs basic number validation, requiring manual type conversion.
00:31:12.540 I also found that figuring out the test preload was challenging; making this process automatic would simplify writing tests. Additionally, the MRuby test runner runs all tests without offering the option to execute a single test or a subset of tests.
00:31:29.340 Without this feature, making large modifications becomes significantly harder, as users must manually filter through tests to determine which test values are significant. Thank you for your time, and are there any questions?
00:31:46.520 Yes, the core question revolved around what is meant by passing data out-of-band for testing. Since you cannot pass data directly across Docker without involving file transfers, I set up the process to write a file containing required test setup data and load it within MRuby.
00:32:29.660 The next question was about the robustness of support for linking dynamic libraries. Yes, you can specify paths; the MRuby gem specification provides support for cross-compiling, allowing you to determine where the required libraries are located for different platforms.
00:32:45.440 For example, when I cross-compiled, the build would organize outputs into platform-specific directories, and I could direct it to find these libraries there. The next question was whether users can pass input if a library isn’t where expected.
00:33:09.180 I believe you would need to edit the buildconfig; I don't think overriding with CC, LD Flags, or library paths is directly possible. As for memory usage, I did not quantify the differences between CRuby and MRuby.
00:33:22.740 My CRuby prototype only runs briefly and performs minimal tasks, but I conducted a testing session of an MRuby curl patch that involved persistent connections, and the memory usage did not grow significantly.
00:33:39.080 As for the time it took to iterate from the first solution to the last, I was already familiar with building command-line tools in CRuby. The main challenge lay in understanding how to properly implement MRuby and the various pieces involved.
00:34:02.570 Writing the implementation of the command-line tool became straightforward once I'd grasped how to work with curl. Overall, I probably spent three to four days developing the overall implementation.
00:34:22.370 The next question was regarding the upgrade path for customers. How would I release a new version of the command-line tool? Since it is a single file, I can upload the binary directly and instruct users to download it.
00:34:39.340 Alternatively, if releasing on Linux, packaging it might be beneficial, especially when dynamic linking is involved. Ultimately, the release method would depend on how I'd like to distribute the product to end-users.
00:34:56.080 There was a question about notable command-line tools created with MRuby CLI. I recall a discussion at RubyKagi about Hakone, a tool for setting up containers. Unfortunately, I don’t have many more examples off the top of my head.
00:35:17.570 Terrence mentioned there's a wealth of tools in Japan, especially compared to the US. One notable example was a monitoring tool discussed in a RubyKagi talk. Lastly, there was an inquiry about how I cross-compiled libcurl.
00:35:41.230 The only steps needed were extracting the relevant compiler configurations provided by MRuby CLI and incorporating them into the configure script accompanying the libcurl source.
00:35:54.560 If you'd like more specific guidance on this topic, I can demonstrate it after the talk. Finally, a question arose regarding how MRuby differs from CRuby, and how both relate to Ruby.
00:36:07.490 CRuby is synonymous with MRI; syntactically, MRuby and CRuby are very similar, but there are some bugs present, and MRuby does not support every feature CRuby does. In particular, certain aspects may not align perfectly; for example, MRuby lacks automatic argument checking for keyword arguments.
00:36:25.830 Some global variables might also behave differently, leading to situations where certain libraries may not be compatible. I submitted a patch addressing an instance where a library did not expose optional captures properly.
00:36:43.140 In summary, while there are some gaps to be aware of, MRuby remains familiar to Rubyists to a large extent. Regarding the availability of require, you just list your MRuby gems in your specification, and they will be compiled in automatically.
00:36:57.370 Although some metaprogramming tricks you can leverage in CRuby may not apply in MRuby, as everything is compiled in and loaded at once, you don't need to worry about opportunistic loading. However, it does require a shift in your thinking concerning metaprogramming.
00:37:19.430 As for how many platforms I’ve built it for, I focus on accommodating Fastly's customer base across diverse operating systems. MRuby CLI has this capability built in.
00:37:34.570 Initially, I disabled Windows support to save time, but I can easily add support for other platforms later. Currently, I'm building for four platforms: Linux and OS X, both 32-bit and 64-bit.
00:37:54.650 There is no reason to disable support unless there is a specific testing need to focus on a particular platform. In general, MRuby CLI handles cross-compilation effectively.
00:38:10.000 Finally, concerning contributions to MRuby compared to CRuby, MRuby's internals are divided into multiple smaller gems. This structure allows for easier navigation and fixes for specific issues.
00:38:29.020 If you need to correct a bug in an array method, you can easily find where the change needs to be made, minimizing the complexities involved with contributing.
00:38:48.660 Terrence noted that MRuby is fully on GitHub, eliminating the hybrid Git-Subversion structure of CRuby. While the community size is smaller, it remains reasonably active.
00:39:05.510 At this point, I believe there are no further questions. Thank you all for your time and attention.