LoginSignup
16
5

More than 1 year has passed since last update.

Dockerを使用したWebアプリケーションの開発環境を改善し開発者体験を良くする

Last updated at Posted at 2022-12-19

LITALICO Advent Calendar 2022 18日目の記事です

対象読者

  • 開発者体験をもっと良くしたいと考えている開発者・マネージャー
  • 開発環境をもっと早くしたいと考えている方
  • 最近RailsやLaravelの起動が遅くて困っている方
  • フロントエンドのビルドが遅くてイライラする方
  • Mac使用者(Windowsユーザーの方にも参考にできる部分はあると思います)
  • Docker DesktopやRancher Desktopの遅さにうんざりしている方

この記事について

この記事は「開発環境を改善し開発者体験を良くする」をテーマとしており、
開発環境を遅くしている原因と、それに対する解決策が書いてあります。

想定は以下の通りですが、そうでない場合も有効な案はあると思います。

  • バックエンドはRailsやLaravelなどのWebフレームワークを使用している
  • フロントエンドはVueやReactなどのフロントエンドフレームワークを使用している
  • Dockerを使用している

ぜひ、自分だったらどうするか、自分だったらここが気になる、早速明日から実践してみよう、など考えながらお読みください。
また、よりより解決策がある場合にはぜひともコメントをください。

全体がそれなりに長いので、気になったところだけ読むのもおすすめです。

この記事は2022年12月に書かれた記事であり、時間が経てば、ソフトウェアの内部仕様や流行っているツールは移り変わります。
100%鵜呑みにするのは危険です。

そもそもなぜ開発者体験が重要なのか?

Developer eXperience(開発体験) とは開発者が開発業務をしているときに経験する体験で、
たとえば開発環境、組織、システム、そして評価体制が含まれます。

平たく言うと、どれだけ快適に開発を進められているかです。

開発者体験の向上により得られるメリットとしては、いくつかあり

  • ユーザーへ早く価値を届けられる
  • 優秀なエンジニアの離職率を下げられる

などが挙げられると思います。

開発者体験の向上のための施策はいくつかあり。

などが挙げられると思います。
その中でも今回は、個人からでも小さく始めやすい「開発環境を改善し開発者体験を良くする」事をテーマとして扱います。

「アプリケーションの立ち上がりが遅いと、正直イライラするし、ローカルでテストやリントを実行するのが億劫になる」
みたいな経験は多くの方があると思います。

私が開発者体験(DX:Developer eXperience)を大切にしたい理由
DX Criteria 策定の目的とビジョン

前置きが長くなりました、早速本題に入っていきます。

アプリケーションの起動速度改善

アプリケーションを長く開発していると、ページ数が増えてきたり、パッケージを数多く入れていたりすると、起動が遅くなる事があります。
初めは許容できてもだんだん許容範囲を超えてくる可能性もあります。
今回の場合はRailsですが、他の言語であれフレームワークであれ同様の流れで改善することができると思います。

起動時のボトルネックが放置されている問題

なぜ遅いか?

基本的に読み込みやinitalizeするファイルが多かったり、そのどれかが遅かったりすると、全体として遅くなります。
パッケージを入れすぎたりするとなりやすいです。

解決案

ボトルネックを把握して解消する。
RailsではBumblerを使用すれば大まかなボトルネックの掌握が可能です。

インストール

bundle add bumbler --group "development"

使い方

Rails: Track load-time of initializers
See how slow your app's initializers are (./config/initializers/*), as well as the initializers for any engines you rely on.
$ bumbler --initializers

Show all loaded gems
$ bumbler --all

こんな感じの計測結果を見ることができます。

$ bumbler --all
[##########################################################################################################################################]
(4/7) bumbler...

4 of 7 gems required
  pending:  bootsnap
  pending:  rack-cors
  pending:  bumbler
      1.95  puma
      4.12  sqlite3
     40.40  debug
    139.20  rails

ボトルネックを把握すれば

  • 普段使わないものをオフにしておく
  • より詳しく調査してボトルネックを修正する
  • ボトルネックが見つからなければ他の施策に移る

等の対策が取れます。
パッケージの放置は開発環境のスピードが遅くなるだけに止まらず、
バージョンアップの妨げや、セキュリティリスクにもなるため定期的に棚卸しをする、Dependabotを導入する等の対策を強くお勧めします。
Ruby/Railsアップデートを乗り越える戦略と攻めのリファクタリング

ざっくりとしたボトルネックの把握後のより具体的な調査と修正に関してはこの記事にすごく良く書かれています。
ぜひご参照ください。
Railsの起動時間を7分の1にした話

ファイルが多い問題

なぜ遅いか?

基本的にファイルが増えるほどパスの読み込みやコンパイル等に時間がかかります。

解決案

キャッシュを使用する。

Railsの場合5.2.0 betaの時から、bootsnapというキャッシュをしてくれるgemが入っています。

On the topic of performance, Rails now ships with Bootsnap in the default Gemfile,
created by our friends at Shopify. It generally reduces application boot times by over 50%.

公式曰く、50%ほど起動時間を改善してくれるそうです。

なぜ速いか?

Bootsnap optimizes methods to cache results of expensive computations, and can be grouped into two broad categories:
    - Path Pre-Scanning
        - Kernel#require and Kernel#load are modified to eliminate $LOAD_PATH scans.
    - Compilation caching
        - RubyVM::InstructionSequence.load_iseq is implemented to cache the result of ruby bytecode compilation.
        - YAML.load_file is modified to cache the result of loading a YAML object in MessagePack format (or Marshal, if the message uses types unsupported by MessagePack).

パス読み込みとコンパイル結果をキャッシュをしてくれるから速いそうです。

実際の計測結果はこちらの記事をご参照ください。
bootsnapについて調べてみた

フロントエンドのビルドが遅い問題

なぜ遅いか?

基本的にファイルが増えるほどビルドに時間がかかります。

解決案

ビルドツールの乗り換えを検討する。

バンドラベースであるWebpackは、アプリケーション全体を提供する前に、アプリケーション全体を隅々までクロールしてビルドする必要があるため、
フロントエンドが大規模になっていくと遅くなって行きます。

そのためその課題を解決すべく開発された、ViteやTurbopackへの乗り換えを検討するのはどうでしょうか?
しかしながら、乗り換えがすぐにできるかはコードの状況にもよります。
たとえばRailsの場合、Webpackerで設定されたものを使っているチームが急にビルドツールを入れ替えるのはかなり大変な作業になると思います。
長期戦略のDeveloper eXperience改善施策としていかがでしょうか?

なぜ速いか?

開発サーバがコールドスタートするとき、バンドラベースのビルドセットアップは、アプリケーション全体を提供する前に、
アプリケーション全体を隅々までクロールしてビルドする必要があります。

Vite はまず最初にアプリケーションのモジュールを 2 つのカテゴリに分割することで、
開発サーバの起動時間を改善します: 依存関係とソースコードです。

依存関係の大部分は開発中あまり変更されないプレーンな JavaScript です。
巨大な依存関係の中には、処理コストが極めて高いものがあります(例: 100 ものモジュールを持つコンポーネントライブラリ)。
依存関係は、様々なモジュール形式で出力されることがあります(例: ESM または CommonJS)。

Vite は、esbuild を使用して依存関係の事前バンドルを行います。
esbuild は Go 言語によって開発されており、依存関係の事前バンドルは、JavaScript ベースよりも 10 倍から 100 倍高速です。

ソースコードには変換を必要とするプレーンな JavaScript ではないものが含まれることがよくあり、
頻繁に編集されます(例: JSX、CSS や Vue/Svelte コンポーネント)。
また、全てのソースコードを同時に読み込む必要はありません(例: ルーティングによるコード分割)。

Vite は、ネイティブ ESM を行使してソースコードを提供します。
ブラウザは、実質的にバンドラの仕事の一部を引き受けます: Vite はブラウザのリクエストに応じて、
ソースコードを変換し提供するのみになります。
条件で囲われている動的インポートのコードは、現在の画面で使われる場合のみ処理されます。

スクリーンショット 2022-12-17 17.14.45.png
スクリーンショット 2022-12-17 17.14.52.png

Viteはアプリケーションのモジュールを依存関係とソースコードの2つに分けて処理を行います。

  • 依存関係: 大部分は開発中にあまり変更されない、プレーンなJavaScript -> 高速なesbuildを使用して事前バンドルを行う
  • ソースコード: 変換を必要とし、開発中に頻繁に編集される、JSX, CSS, Vue/Svelte/Reactなどのコンポーネント -> 全てのソースコードを同時に読み込む必要はないため、ブラウザのリクエストに応じて、変換し提供する

モダンブラウザはNative ESMを備えており、ES Modulesを直接理解することができるため、
Viteはビルドの成果物である、複数ファイル・モジュールをバンドルせずに、
そのままブラウザに読み込ませることができます。

esbuildはなぜ速いか?

esbuildの公式サイトでは以下のように述べています。

・It's written in Go and compiles to native code.
・Parallelism is used heavily.
・Everything in esbuild is written from scratch.
・Memory is used efficiently.

Goを使用してフルスクラッチで開発することにより、徹底したメモリ管理と並列処理によりこの速さを手に入れたそうです。

Viteを導入せずとも、webpackにesbuild-loaderを使用し、esbuildを導入することも可能です。
ぜひご検討ください。

esbuild-loader
esbuild-loader 試してみたら開発ビルドが 2〜3 倍速くなった話
esbuild で開発環境の JS をビルドをしたら 55 倍(220sec->4sec)高速になった件について解説させてください

npm遅い問題

npm経由のinstallはそこまで頻度が高いわけで無いのに加えて、
現在はnpmも改善されており、そこまでクリティカルではないですが、一応アイデアとして書きました。

解決案

yarnやpnpmの使用を検討する。
yarnとpnpmではnpmで使用していたプロジェクト設定ファイルpackage.json)がそのまま使えます。
(一部pnpmには標準対応していない場合もあります)
npm と yarn と pnpm 比較(2021年4月版)

なぜ速いか?

Why is pnpm so crazy fast compared to other "traditional" package managers?
pnpm doesn't have blocking stages of installation.
Each dependency has its own stages and the next stage starts as soon as possible.

pnpmはインストール時にブロック化されたステージを持たず、
それぞれの依存関係に応じたステージがあり、できるだけ早く次のステージが開始されるように設計されています。

詳しいベンチマークは公式ページをご覧ください。

ここまでのまとめ

ここまで、アプリケーションのビルドや初期化処理について詳しく見てきました。
特に最近は、フロントエンドのビルド周りの技術の進化が盛んなので目が離せませんね!
次はDockerの速度改善に移ります。

Dockerの速度改善

ここからは、より深ぼって、Dockerの速度改善にまで足を伸ばしてみたいと思います。

Dockerとは何か?

Dockerはアプリケーションをパッケージ化して実行するために、ほぼ分離された環境となるコンテナーというものを提供します。
隔離してセキュリティを保つことから、実行するホスト上に複数のコンテナーを同時に実行することができます。
コンテナーは非常に軽量なものであり、アプリケーション実行に必要なものをすべて持っています。
したがって、その時点においてホスト上に何がインストールされていても、それに影響を受けません。
作業中のコンテナーは簡単に他の人と共有することができ、共有したコンテナーは誰でも同じようにして動作させることができます。

DockerとはDocker社が開発している、コンテナ型の仮想環境を作成、共有、実行するためのプラットフォームです。

なぜDockerを使用するのか?

Docker はアプリケーションの開発、導入、実行を行うためのオープンなプラットフォームです。
Docker を使えば、アプリケーションをインフラストラクチャーから切り離すことができるため、
ソフトウエアをすばやく提供することができます。 Docker であれば、アプリケーションを管理する手法をそのまま、
インフラストラクチャーの管理にも適用できます。 Docker が採用する方法を最大限利用して、
アプリケーションの導入、テスト、コードデプロイをすばやく行うことは、
つまりコーディングと実稼動の合間を大きく削減できることを意味します。

Dockerを使用する事により、
OSレベルのライブラリ、ミドルウェアのバージョン、環境設定等の様々な設定を
開発環境、テスト環境、本番環境、と複数に跨る環境間で同じものにすることが可能です。
その結果として、開発では動いたが、本番では動かない、のようなトラブルを減らすことができます。

また、Dockerを使用するとOSやランタイムのバージョン、その他ミドルウェアのインストールや各種環境設定をコードとして管理する事ができます。
これはInfrastracture as Code(IaC)と呼ばれていて様々なメリットがあります。

  • 変更のログを残しやすい
  • メンバー間でレビューし合える
  • 簡単に配布できる
  • 簡単に壊したり作り直したりできる

歴史的背景

Dockerが誕生する前から、DockerのベースとなっているLinuxコンテナという技術自体は存在していました。
にもかかわらず、コンテナ技術=Dockerの様に扱われている背景として以下のように自分は考えています。

  • ソフトウェア開発が複雑化し、簡単に、複数人の間で同じ環境を共有したいニーズが増えた
  • 作ってからはメンテナンスがメインの従来型のウォーターフォール的な開発から、アジャイル型の開発が流行し、作って修正してが簡単にできる製品のニーズが伸びた
  • それらの課題を解決すべく、ちょうどいいタイミングでDockerがヒットした
  • クラウドプロバイダー等のエコシステムも成熟し、コンテナイメージさえあればアプリケーションをデプロイできる様になった

あくまでも一例ですが、
2022年12月現在、以下の様な条件でWebアプリケーションを作成する場合には、
バックエンドには何かしらのフレームワークを入れて、DBを使用して、キャッシュ用途にNoSQL、全文検索にElasticSearchを使い、それの可視化にKibana、フロントにはReact、VueのどれかとTypeScript、ビルドしたimageをコンテナレジストリにPushして、それをAWS Fargateの様なマネージドサービスにデプロイしたい、と考える方も多いのでは無いでしょうか?
(もちろん絶対的なものではなく、要件やメンバーのスキルによって設計や方針は変わります)

  • フルスクラッチ開発
  • インフラの運用は最低限度に抑えて開発に集中したい
  • 早くリリースしてユーザーからのフィードバックを受けたい
  • クラウドを使用してOK
  • スケールが予想される
  • 全文検索機能が必要

もしその全てを手動でインストール&アップデートするのであれば、開発メンバーの全員が同じバージョンを使うだけでも一苦労しそうです。

コンテナとは何か?

ここまでざっくりとDockerの概要について学んできました。
次は、これからの最適化の理解の手助けのため、コンテナの原理を少しだけ解説します。

コンテナーとは何でしょう。 簡単に言うと、コンテナーとはマシン上の単なる 1 つのプロセスであって、
ホスト上の他のプロセスとは分離された、サンドボックス化したプロセスです。
この分離状態は カーネルの名前空間と cgroups によって実現されています。
それは Linux において長らく実現されてきている機能です。 Docker はそういった機能を活用し利用しやすくして動作しています。

Dockerで立ち上がったプロセスは、あくまでOS上で動く、プロセスの1つに過ぎず、他のプロセスとあまり変わりません。
このように、ホスト上の他のプロセスとは分離された、サンドボックス化したプロセスのことをコンテナと呼びます。
そんなDockerの実現に欠かせない機能として、公式サイトにはnamespacesとcgroupsが挙げられています。

nameapace

namespaceはLinuxカーネルが提供する、リソースを分離する為の機能です。
Container Security Book (Namespace)

cgroups

cgroupsはLinuxカーネルが提供する、グルーピングした特定のプロセス群に対し、リソースの制限を行う為の機能です。
Container Security Book (cgroup)

コンテナはよく仮想マシンと比較されます。
ここまでの話の通り、コンテナが仮想化するのはプロセスです。
ですがVMが仮想化するのはOSです。
このことから、コンテナはVMと比べると軽量です。

スクリーンショット 2022-12-18 0.18.58.png

Demystifying Containers
原理原則で理解するDocker
Linuxのしくみ ―実験と図解で学ぶOS、仮想マシン、コンテナの基礎知識

前置きがかなり長くなりました、それでは早速、速度改善に取り掛かりましょう。

メモリやCPUがあまり割り振られていない問題

なぜ遅いのか?

リソースが足りないから。

改善案

増やす。
あんまりやるとPCに負荷がかかりファンがすごい回ってうるさいので注意。
スクリーンショット 2022-12-18 1.17.17.png

Docker Desktop for Macのマウントが遅い問題

そもそもですが、Docker on Macのマウントのパフォーマンスはよろしくないです。

なぜ遅いのか?

Docker for Macのmount遅い問題まとめ

Docker stuffの David Sheetsさんによる説明
File access in mounted volumes extremely slow, CPU bound - Docker for Mac - Docker Forums

改善案

最新バージョン(4.15 2022-12-18現在)をインストールする。

これは自分がこの記事を書いているときにちょうど発表されたものです。

Docker Desktop 4.15ではMac版で、仮想マシンとホストの間でファイルを共有するVirtiofsが正式版となりました。

これによりMacでのファイルアクセスが大きく高速化することとなり、パッケージのインストールやデータのインポート、
テストなどのさまざまな処理の高速化が期待されています。

VirtiofsはmacOS 12.5もしくはそれ以降のバージョンで利用可能です。

New in Docker Desktop 4.15: Improving Usability and Performance for Easier Builds

Virtiofsは4.6.0の時から設定で有効にする事はできましたが、この度正式版になりました。
Docker Desktop release notes 4.6.0

ボリュームを使用する

以前までのバージョンではこの課題を解決しようとする先人たちのたくさんの努力のあとが見つかりました。

  • ボリュームを使用する
  • バインドマウントオプションのcached, delegatedを使う
  • docker-syncを使う
  • docker volumeでnfs共有したマウントポイントを共有
  • rsyncコンテナを立ち上げて同期
  • d4m-nfsを使う
  • host側のnfsマウントを利用して同期

などが挙げられていました、個人的には、特に追加で設定が不要で簡単に始めやすく、公式がベストプラクティスとして掲載しているボリュームの使用がお勧めです。

アプリケーションデータはどこにどう保存するか

ストレージドライバー によって、コンテナーの書き込み可能レイヤーへデータ保存を行うことができますが、
アプリケーションデータの保存を行うことは避けます。
これを行ってしまうと、コンテナーのサイズが増えることになり、I/O 観点で言えば、
ボリュームやバインドマウントを用いることに比べて非効率なものになります。

そのかわりに、データ保存は ボリューム を利用します。

バインドマウント を用いるのが適当な例として、開発時での利用が考えられます。
開発時には、ソースディレクトリや生成したばかりのバイナリを、
コンテナー内にマウントしたくなります。
本番環境ではボリュームを利用しますが、本番環境がマウントする同じ場所を、開発環境時はバインドマウントによりマウントします。


適切なマウント方法の選定

version: '3'
services:
  backend:
    build: .
    ports:
      - '3001:80'
    command: "bundle exec rails s -p 80 -b 0.0.0.0"
    volumes:
      - .:/app # ソースコードをbind mount
      - bundle:/usr/local/bundle # gemのインストール先をvolumeにする

node_module等でも同様に使えます。

そもそもMacでDockerを動かすにはオーバーヘッドがある

なぜ?

これまで解説してきた通り、DockerはLinuxの機能を用いて、コンテナ型の仮想環境を作成するソフトウェアであり、MacはLinuxではないので、直接Dockerを動かせない為です。

先ほどDockerとVMを図にして説明しましたが、実はMacでDockerを動かすときはVM上にコンテナが起動しています。
コンテナとVMは違う技術として紹介される事も多いですが、
実は内部的にはDockerをLinux以外のOSで動かすには、VMの力を使っています。

スクリーンショット 2022-12-18 1.59.37.png

Rancher DesktopとFinchは内部でLimaを使用しています。

Rancher Desktop
コンテナ開発用のオープンソースクライアント「Finch」のご紹介
Lima: Linux virtual machines (on macOS, in most cases)
Lima is now a CNCF project 🎉

Docker Desktop for Macでは、2022-11-10の4.14.0からVirtualization frameworkがデフォルトで使われています。
Docker Desktop release notes 4.14.0
Virtualization Framework
macOSのコンテナ開発環境におけるVirtualization frameworkの採用

しかし、VMを用いているので、当然ながらVM内に入らなければdockerコマンドが打てません。
この仕組みを知るためには、Dockerのアーキテクチャを知る必要があります。

Docker アーキテクチャー

Docker はクライアントサーバー型のアーキテクチャーを採用しています。
Docker クライアント は Docker デーモンに処理を依頼します。
このデーモンは、Docker コンテナーの構築、実行、配布という複雑な仕事をこなします。
Docker クライアントとデーモンは同一システム上で動かすことも 可能 ですが、別のシステム上であっても、
Docker クライアントからリモートにある Docker デーモンへのアクセスが可能です。
Docker クライアントとデーモンの間の通信には REST API が利用され、
UNIX ソケットまたはネットワークインターフェイスを介して行われます。
もう 1 つの Docker クライアントとなるのが Docker Compose です。
これを使えば複数コンテナーからなるアプリケーションを動作させることができます。


Docker デーモン
Docker デーモン(dockerd)は Docker API リクエストを受け付け、イメージ、コンテナー、ネットワーク、ボリュームといった Docker オブジェクトを管理します。 また Docker サービスを管理するため、他のデーモンとも通信を行います。

Docker クライアント
Docker クライアント(docker)は Docker とのやりとりを行うために、たいていのユーザーが利用するものです。
docker runのようなコマンドが実行されると、Docker クライアントはdockerdにそのコマンドを伝えます。
そしてdockerdはその内容を実現します。 dockerコマンドは Docker API を利用しています。
Docker クライアントは複数のデーモンと通信することができます。

Docker architecture

Dockerはクライアント・サーバーアプリケーションであり、コマンドサービスは次の2種類に分かれています。

  1. Docker Client: dockerd に HTTP 通信で指示を出す為のCLI
  2. dockerd : コンテナの管理等をしている Engine API (HTTP) を持ったデーモンプロセス

Macから、dockerコマンドを叩くと、/var/run/docker.sockファイルによるUNIXドメインソケット通信を行います。
その際に Docker DesktopではVMに入らなくても、疎通が可能な様に、
/var/run/docker.sockにはシンボリックリンクが貼られており、dockerコマンドを打つと、VM中のdockerdに伝達されるようになっています。

$ ls -l /var/run/docker.sock

lrwxr-xr-x  1 root  daemon  41 11 22 12:45 /var/run/docker.sock@ -> /Users/[User Name]/.docker/run/docker.sock

より詳しい内容はこちらをご覧ください。
Docker / Docker Desktop / Rancher Desktop って何が違うの?
UNIXドメインソケット【UNIX domain socket】

ここまでの仕組みがわかれば「そもそもMacでDockerを動かすにはオーバーヘッドがある」の意味がわかるかと思います。
それではお待ちかね、それを解決する案を提示します。

解決策

  • そもそもLinuxやWSL2を使用する

ここまで引っ張ってきて大変恐縮ですが、MacでDockerを動かすよりもやはり速いです。
WSLはいいぞ

AWS

EC2を1つ用意しsshが出来る様にすればすぐに使い出せます。
環境変数に

DOCKER_HOST=ssh://[user]@[host name]:[port]

とexportしておけば、ホストが切り替わりlocalでdockerコマンドを叩いても、AWS側に指示を出すことができます。
docker daemonとclientの分離(dockerを別のマシンで動かして接続する)

Visual Studio CodeのRemote SSHを使用すると、リモートでのファイル操作が捗るのでおすすめです。
Remote Development using SSH
Visual Studio Code で Remote SSH する。

自作PC

OSのインストールまでできればそこまでEC2と変わりません。
完全に我が子可愛さですが本当に可愛いです。
スクリーンショット 2022-12-18 23.45.14.png

まとめ

特に最近はフロントのビルドツールやコンテナ周りの技術革新のスピードがかなり早く、
インパクトのあるプロダクトがどんどん出てきています。
実際に自分が記事を書いている間にDocker Desktopからリリースがあり、自分の書いていたことが過去の事になりました。
ですがだからこそ、目の前のツールの仕様だけをただ闇雲に覚えて使える気になるだけではなく、
きちんとその中身や原則を理解した上で使いたい。
そんな思いでこの記事を書きました。
より本質的な理解を通して、ツールや時代に流されるのではなく、乗りこなしてやりたいと思うこの頃です🌊🏄🏻🌊
それとわりかし業務で行う普段のWebアプリケーション開発では、高レベルの言語や抽象化されたクラウドを触ることが多く、別にそれでも困っていませんでしたが、今回のように根本から早くしたいと願うのであれば、体系的なLinuxや低レイヤの知識が必要なのだなと強く感じました。
この記事を書くのに本当に多くの先人達の記事を参考にさせて頂きました。
改めてお礼を申し上げます。

明日はLITALICO Advent Calendar 202219日目の記事です。
@joy04d, @mayushibata が担当します。どうぞお楽しみに!

おまけ

弊社では絶賛採用活動中です!!

16
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
5