TypeScript3.8以降のECMAScript private fieldについて
現状targetがesnextじゃないならあまり使わないほうがよい
Reason
以下のようなシンプルなケースを考える。
class A { #field = 1; something() {return this.field;} }
target
がesnext
以外だと
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); }; var _field; class A { constructor() { _field.set(this, 1); } something() { return __classPrivateFieldGet(this, _field); } } _field = new WeakMap();
というアウトプットになる。
privateフィールドを実現するためにWeakMap
を活用しており、アクセスのたびにWeakMap
を検索するためとにかく遅い。
そのため、よほどの理由がない限り(多分ほぼ無い)使うべきではない(現状はね)
2019 Javascript engine 俯瞰
2019 Javascriptエンジン俯瞰
こんにちは 2019 Javascript Advent Calendarの11日目です
2019はJSエンジンが新たに2つもリリースされた
まずFacebook産のhermes
もう一つがFFMPEG作者のbellardが実装したquickjs
この2つを見ていこうと思う
ちなみにhermesは以前にも書いたので正直あまり書くことは無い
http://abcdef.gets.b6n.ch/entry/2019/07/22/142510
特徴
hermes
- C++
- FacebookがReact Nativeの高速化用に実装したエンジン
- レジスタマシンのバイトコードインタプリタを搭載
- flowを解釈できる
- commonjsを解釈して実行できる
- バイトコードのexportとimportも可能でスタートアップタイムを高速化することが可能
- JITはx86_64の実装はあるが使うパスが無いためOFFになっている
- let/constやclass、ES Moduleといった機能はサポートされていない
- Reflectionやwith、Symbol.speciesといったものは今後もサポートしない
- 一部Ecma262と仕様が異なる
quickjs
- すべてCで書かれている
- とにかく小さい(x86で190 KiB)
- スタックマシンのバイトコードインタプリタを搭載
- Ecmascript2019はほぼ完全にサポートEcmascript2020(予定)もサポート
- BigInt/BigFloatもサポート
- jsをExecutableにコンパイルできる
- バイトコードインタプリタ
実装
パーサ
hermes
hermes
はパースしながらASTを生成する
hermes
のASTはESTreeの仕様に準拠しているのでわかりやすい
quickjs
quickjs
は非常にドラスティックな実装となっており、パースしながらなんとBytecode
を直接出力する
ファイルサイズ削減のためにASTをスキップしていて、完全にコンパクト方面に振り切っている感じ
ここらへんはエンジンの色がでていて面白い
Bytecode
hermes
hermes
のバイトコードはBytecodeList.def
に全てまとまっており、全てマクロ呼び出しで後々再利用できるようになっている
hermes
のバイトコードは可変長で、オペランド数はMaxが6、オペランドのサイズも可変長
NewObject
等の割と大きめな命令から、BitXor
の様なプリミティブ命令まで実装されている
quickjs
対するquickjs
のバイトコードはquickjs-opcode.h
にすべてまとまっており、こちらも同じくマクロ呼び出しとなっている
まあ言語エンジンを実装するときは大体こうなるよね
quickjs
のバイトコードだが、まあ大体hermes
と同じような粒度かな
一部push_i32
のようなスタックマシンぽいコードもあったり
Bytecode
は両者ともわりかし似ている感じ
Object Model
hermes
以前も書いたが、hermes
はNaN-Boxingで実装されている(NaN-BoxingについてはFacebookのHermes Javascript Engineについてにも書いた)
hermes
のヒープオブジェクトはGCCell
クラスを頂点としたモデルになっている
GCCell | +--------+------+-----------------------+-----------+----------------+------------+--------------+--------------+-----+-----+---------+ | | | | | | | | | | | | JSObject Domain VariableSizeRuntimeCell HiddenClass PropertyAccessor HashMapEntry OrderedHashMap OrderedHashMap Type1 Type2 EmptyCell FinalizerCell | |--------+--------------+------------------------------------+---------+---------------+-------------+----------+------+-------+-----------+---------+-----------------+--------+----------------+-----------------+------------+----------------+ | | | | | | | | | | | | | | | | | | Callable RequireContext HostObject ArrayImpl JSArrayIterator JSArrayBuffer JSDataView JSDate JSError JSGenerator JSMapImpl JSMapIteratorImpl JSRegExp JSTypedArrayBase JSWeakMapImplBase PrimitiveBox JSStringIterator SingleObject | | | | | +-------------+-----------------------------------+ +---------+ | | +--------+--------+---------+ | | | | | | | | | | | BoundFunction NativeFunction JSFunction Arguments JSArray JSTypedArray JSWeakMapImpl JSString JSNumber JSBoolean JSSymbol | | NativeConstructor NativeConstructor JSGeneratorFunction GeneratorInnerFunction
JSObject階層以外は省略したが、hermes
は上記の様なオブジェクト階層を持っておりまあ割と複雑
GCCell
クラスはVTable
というオブジェクトに関する情報を保持しているクラスをラップしており、VTableが実際にGC用の型情報やマーク情報を持っている
ただ、VTable
クラス自体はGCによってアドレスが変わる可能性があるため、GCCell
クラスがハンドルの役目を果たしている
そのため、GCCell
はメンバを持たない
ランタイムでのオブジェクトの型情報はその名の通りHiddenClass
が保持しており、V8っぽくHiddenClass
の中にプロパティのキャッシュを持っていたりする
当然プロパティ追加時のTransition
も実装されていて、ちゃんとhidden classとして機能している
{symbol_id} = internal variable name id {property_flag} = enumerable|writable|configurable|... +------------------------+ | HiddenClass(root) | +------------------------+ | | (Transition HiddenClass(propertyA)) | +------------------------+ | HiddenClass(propertyA) | +------------------------+ | (TransitionMap {symbol_id}_{property_flag}: HiddenClass(propertyB), {symbol_id}_{property_flag}: HiddenClass(propertyC)}) +-------------------------+ | | +------------------------+ +------------------------+ | HiddenClass(propertyB) | | HiddenClass(propertyC) | +------------------------+ +------------------------+
こんな風に1つの派生だけの場合はTransitionオブジェクトを直接参照して、複数のTransition
がある場合はsymbol_id
とproperty_flag
のキーとHiddenClass
を直接ハッシュマップとして持つことでTransition
を実現している
quickjs
quickjs
はCなので明示的なオブジェクト階層はないもののJSObject
が多くのJS型のベースとなっておりメンバをunion
を選択する形となる
またJSValue
という空のstruct
をGeneric
な値として利用しており、内部に持つ値はvoid*
ではなくJSValue*
型で保持している
JSObject | +-JSBoundFunction +-JSCFunctionDataRecord +-JSForInIterator +-JSArrayBuffer +-JSTypedArray +-JSFloatEnv +-JSMapState +-JSMapIteratorData +-JSArrayIteratorData +-JSRegExpStringIteratorData +-JSGeneratorData +-JSProxyData +-JSPromiseData +-JSPromiseFunctionData +-JSAsyncFunctionData +-JSAsyncFromSyncIteratorData +-JSAsyncGeneratorData +-func +-cfunc +-JSTypedArray +-array +-JSRegExp
こんな感じでJSObjectにunion
を持っている
クラシックなunion
ベースのモデルなので読みやすくていい
JSObject
はルートの構造体となるので、型情報や、GCMark等の情報を保持している
型情報は全てJSObject
内のJSShape
というstruct
に持っている
JSShape
はプロパティ自体も保持しているが、現在のプロパティのバージョンをハッシュとしても持っており、プロパティが変化することでJSShape
内のハッシュ値も変化する
さらに次のハッシュ値をプロパティのメモリアドレスから計算して、前のJSShape
へ持たせることで、次のJSShape
を検索することが可能になり、Transition
を実現している
+-----------+ | JSShape A | |A(next_hash = null)| ... +-----------+ | | Add property | +-----------+ | JSShape B | |A(next_hash = 3) | |B(next_hash = null)| +-----------+
だいぶ簡略化して描いているが、next_hashに次のインデックスを格納するイメージ(本当は直接インデックスは格納しない)
VM
hermes
再掲となるが
VMで利用されるレジスタは一応無限の仮想レジスタとなっているが、単純なリニア生存区間解析をCFG上で行っている
一応無制限と書いたのは、Bytecodeのレジスタインデックスが1byteしか受けつけないので、実質256までしかレジスタ割付ができないからである
https://github.com/facebook/hermes/blob/master/doc/Design.mdに書いてあるが、Facebook調べでは256以上のレジスタを使う関数は 見当たらなかったらしい(ので大丈夫ということか)
quickjs
スタックマシンなので単純にstack pointer
を伸縮している
両者ともラベルアドレスへのダイレクトジャンプを実装している(もちろんラベルのアドレスが取得できないコンパイラ向けのswitch-caseもある)
ちなみにどんな機能かというと
Label_A: { ... } Label_B: . . . const void* dispatch_tables = {&&Label_A, ...} goto *dispatch_tables[1]
とすることでなんとlabel
へとgoto
できるという素敵な仕様
これを使うことでジャンプの度にcmpする必要がなくなる(indexの計算のみ)さらにCPUの分岐予測もいらないので、VMのループが高速化される
GC
hermes
Generational
なMark-Sweep GC
でしたそれ以外特に言うこともないかな
オブジェクトグラフをひたすらVisitor
がたどっていくprecise GC
の実装となっていた
quickjs
こっちはちょっと特殊で参照カウント + mark-sweep
となっている
最初にオブジェクトグラフを巡回して参照カウントをdecref
したのち、参照カウントが0のオブジェクトをdelete_list
に詰め込んで回り、全て回収完了後にJSObject
を解放する
Pythonの方式と同じ方法でオブジェクトを削除している
すべての割り当てられたオブジェクトはobj_list内にあるので、それをtmp_obj_list
にコピーして、
巡回中の参照カウントが1以上のオブジェクトはobj_list
に戻す
+----------------+ | global_env | +----------------+ | | +----------------+ | | tmp_obj_list | | +----------------+ | | | | +------------+ +------------+ | |-----| objectA 1 |------->| objectB 1 | | | +------------+ | +------------+ | | | | | | +------------+ | | +-->| objectC 2 | | | +------------+ | REF | +------------+ ↑ |==============> |-----| objectD 2 |--------------+ | +------------+ | | +------------+ REF | ---->| objectF 2 |<=====+ | +------------+ | +------------+ | |-----| objectE 1 |---+ | +------------+ | +------------+ REF | ---->| objectG 2 |<=====+ +------------+
これが巡回前 参照されているのはすでにObjectDのみの状態で、objectFとobjectGは循環参照している
+----------------+ | global_env | +----------------+ | | +----------------+ | | tmp_obj_list | | +----------------+ | | | | +------------+ +------------+ | |-----| objectA 0 |------->| objectB 0 | | | +------------+ | +------------+ | | | | | | +------------+ | | +-->| objectC 1 | | | +------------+ | REF | +------------+ ↑ |==============> |-----| objectD 1 |--------------+ | +------------+ | | +------------+ REF | ---->| objectF 1 |<=====+ | +------------+ | +------------+ | |-----| objectE 0 |---+ | +------------+ | +------------+ REF | ---->| objectG 1 |<=====+ +------------+
これが巡回後
親が外部から参照されているオブジェクト以外は参照カウントが0になる、そして親が0の場合は回収されるので、循環していても関係なく削除できるようになる
ちなみにquickjs
はメモリ割り当てに通常のmalloc
を使用していて、malloc
の管理ヘッダ分のサイズも計算して割当中のオブジェクトサイズを保持している
メモリを回収する場合も単純にfree
を呼び出しており、メモリ管理は基本的にすべてglibc
にまかせている
まとめ
簡単に2つのエンジンを俯瞰したが、quickjs
は軽い実装ながら面白い箇所が多く、またコードもきれいで読みやすいのでおすすめ
quickjs.c
にほぼ全てのコードが書かれているので読むのも簡単だと思う
hermes
は普通に読むの面倒だし、そんなに面白いことも無いかもしれない
疲れました
では
FacebookのHermes Javascript Engineについて
最近、JSエンジンが何故かいくつか出て来たのでいっちょ見て見ることに
最初はFacebookが実装したjavascriptエンジンHermes(エルメス)の実装を見てみた
面倒くさいのでコードとかは引用しない
概要
どうやらReactNativeの高速化のために実装したエンジンのようだ
ReactNative側ですでに利用できるっぽい
売りとしてはバイトコードを出力・読み込みができるのでスタートアップタイムを高速化できるということらしい
commonjs
の静的解析機能もついており今風な感じ
仕様
サポートされる仕様はhttps://github.com/facebook/hermes/blob/master/doc/Features.mdにある
サポートしている言語仕様はES5 + α
let/constやclass、ES Moduleといった機能はサポートされていない
とりあえずbabelとかts使うから動くでしょ といったところか
またReflection
やwith
、Symbol.species
といったものは今後もサポートしないらしい
Function.prototype.toString
もソースコードを返さないなど、割と必要ないものはバッサリ切った感じ
React Navtive
のためと言っているのでいいのかな
ビルド
成果物が結構あるのに解説があんまないので困る
以下が成果物
- bin/hermes
- ソースコードから実行する場合はこのバイナリ
- bin/hermesc
- バイトコードを生成する場合はこれ
- bin/hdb
- デバッガ
- bin/hermes-repl
- repl
- bin/hbcdump
- バイトコードのダンパー
- bin/hbc-deltaprep
- バイトコードのフォーマットを差分形式に変換する
- bin/hbc-diff
- バイトコードのdiff
- bin/hbc-attribute
中身
とりあえず概要をみてみた
以下が基本的なパスとなりそう
ソースコード => AST => IR => (最適化) => バイトコード => 実行
またバイトコードを読み込むことで
バイトコード => 実行
のパスもある
AST
Node
があってBNF定義に近いASTがある
要は普通のAST実装
ただdecoratorで実装しているっぽいのでちょっと読みづらい
Visitorパターンでトラバーサルする
IR
ASTが生成されたらIRを生成する
hermesのIRはCFGも兼ねるグラフとなっている
このIRは結構低レベルでInstructionレベルまで表現している
バイトコード
IRから変換して生成される
OPコードは1byteでOperandは可変長
VM
バイトコードを実行するVMはレジスタベースの仮想マシン
教科書通りGCC拡張のアドレスへのgotoとLabelアドレスの取得機能でループ無しで実装されている
ただしサポートされないコンパイラ向けにループとSwitch構文での実行機能も持っている
レジスタ
VMで利用されるレジスタは一応無限の仮想レジスタとなっているが、単純なリニア生存区間解析をCFG上で行っている
一応無制限と書いたのは、Bytecodeのレジスタインデックスが1byteしか受けつけないので、実質256までしかレジスタ割付ができないからである
https://github.com/facebook/hermes/blob/master/doc/Design.mdに書いてあるが、Facebook調べでは256以上のレジスタを使う関数は
見当たらなかったらしい(ので大丈夫ということか)
ABI
Facebook HermesはNaN-Boxingでオブジェクトを表現している
NaN-Boxingとは64bitフロートのNaN定義を利用したポインタと数値の表現方法
NaNは上位17bitが1であれば下位のBitがどんな値でも問題ないので、それを利用してポインタやタグ情報を埋め込んでいる
ただし64bitシステムだとポインタは64bit利用するので埋め込めないように見えるのだが、現状64bitシステムであっても全ての領域は使い切っていないので問題なく収まる
(Linux x86-64で48bitまで利用と書いてあった)
ただし、そこまで考えなくてもVMのヒープが47bitで収めきればいいだけなので、そこまで問題は起きないだろう
Hidden Class
今のご時世、ShapeやらHidden Classは高速化に必須だろう
HermesもHidden Classを実装している
V8と同じようにオブジェクトレイアウトをclassとして認識して、プロパティの追加・削除等のレイアウト変更を行うと新たなクラスが生成されるようになっている
新たにHidden Classを生成した場合はtransitionをした結果を保存して検索するような仕組みになっている
IC
複雑なインラインキャッシングは実装されていないように見える
一応プロパティのキャッシュとhidden classの保持はしているのでプロパティキャッシュ自体はしているが
正規表現
正規表現は独自バイトコードを使ったVM型のエンジンを実装している
既存のエンジンのJITに比べるとちょっと見劣りするかもしれない
最適化
主にIRに対しての最適化が実行されている
内容はIRのloweringとかinline化など
面倒だったのであんまりちゃんと読んでない
JIT
実はJITエンジンももっている がまだリリースされていなのでOFFになっている
中を見た感じx86-64から提供する様だ
ARMは...?
まとめ
React Nativeのために作ったというだけあって、色々省いてあったりバイトコードローディングなど工夫があった
後発のエンジンなだけにcommonjs対応してたり、今風な感もあって面白い
ただ、言語仕様がEcmascriptのエディションと変わってきてしまっている(withがなかったり)のはちょっと気になっている
最近感じていることだが、Ecmascriptも仕様が大きくなってきていてエンジンの実装も大変になってきているし、
使用目的がはっきりしているエンジンにとっては多分サポートする意味の無い機能(withとかnot strictなモード、多言語対応等)は省いたEcmascriptのサブセットがあってもいいのかもしれない
というか個人的にはほしい
JavascriptのObjectリテラルとJSON.parseについて
V8のJSON.parseについて
最近(ちょっと前か)話題のオブジェクトリテラルよりもJSON.parse
のほうが早い件について。
その理由を内部実装の観点から書く。
また注意点を最後に書いた。
話題のブログは以下から https://v8.dev/blog/cost-of-javascript-2019
パースについて
V8はjavascriptコードをパースするにあたって、Lazy Parseを行っている。
Lazy Parse
javascriptはブラウザという特殊な環境で実行される言語であるため、パースにも少々工夫が必要になる。
基本的にV8はすべてのソースコードをパースしない。
一旦グローバルスコープにあるものだけをちゃんとパースして、それ以外はPreParserというパーサで関数だけをかき集める。
function foo() { } function bar() { function baz() { const value = 100; } }
この例だと、foo
とbar
、baz
という関数が存在していることはパースするが、const value = 100
は一切パースせずASTを生成しない。
baz
が呼び出されて初めてconst value = 100
がパースされる。
How to skip parsing
ではどのようにコードのパースをスキップしているかというと、V8のパーサは手書きの再帰下降構文解析器になっており、
ParserFunction
のようなメソッドが一杯あるクラスになっている。
さらにパーサはPreParser
とParser
に分かれており、それをテンプレート引数で受け取って実行するParserBase
クラスが各パースのエントリーポイントを定義している
template <typename Impl> class ParserBase<Impl> { ... protected: ... ExpressionT ParseFunctionExpression(); ... private: Impl* impl() {return parser_;} Impl* parser_; };
のような感じでParserBase
がパーサのBNFに対応する各パース段階のエントリーポイントを持つ。
その中でimpl()->ParserFunctionLiteral()
のような形で実際のパーサを呼び出してパース処理を行っている。
Parser and PreParser
さて実際にパース処理を行うパーサは2種類ある。
PreParser
はASTを生成せずに関数名やその他情報を集めるだけのパーサで、Parser
は実際にASTの生成を行うパーサとなっている。
ここで大事なのがPreParser
の実装である。
PreParser
はASTは生成しないものの実際に構文のパースは行う。
そのため、V8ではjavascriptのソースコードは2回パースされることとなる。
ここでJSON.parse
の特殊性が影響してくる。
JSON.parse
JSON.parse
の第一引数には文字列のJSONオブジェクトが渡される。
これがキモになる。何故かと言うと、文字列のパースコストは非常に低いのだ。
なぜなら文字列はパース段階ではなく、スキャン段階でトークン化されるため、何度パースされても文字列リテラルのトークンを判定するだけで処理が完了する。
そのため、パースの負荷が非常に低くなる。
また、JSON.parse
自体もランタイムで処理が行われるために、実際に呼び出されるまで処理が行われず、不要なパースをすべてスキップすることができる。
すなわち手動でLazy Parsingをしているに等しい状態となっている訳だ。
これらの要因が組み合わさってオブジェクトリテラルをべた書きするよりも、JSON.parse
の方が早くなるという不思議な現象が発生する。
注意点
ただしこの方法には注意点があって、あくまでこの手法はstartup timeの高速化しかできない。
実際の実行時間はJSON.parseのほうが遅くなるので、FMPの高速化には寄与するかもしれないが、すべてのオブジェクトリテラルをこれで置き換えると結構遅くなりそうなので注意。
あくまでパースのLazy化と考えたほうが良い。
ちなみにJSON.parse
が遅いのはASTを作らず、毎回パースの手間がかかるから。
一度しか実行されないケースに関してはそこまで速度の差はでない。
一応ベンチマーク
https://jsperf.com/json-parse-vs-object-literal-in-parser
自分の環境では80%くらいJSON.parse
が遅い
React hooks for React-Redux
久しぶりに時間が少しあったので、今更ながらReact hooksで遊んでみた。
Redux
とりあえず、useReducer
とか触ってみたけど、redux
勢には物足りない感...
reduxをReact hooksで使えるやつ探したらfacebookが出してるredux-react-hookを見つけた。
使ってるうちに、こんな仰々しいものじゃなくていいんだよなという思いが拭えず...
結局React Hooksの勉強がてら全てをhooksでこなすrrhというYet another redux-hookを作ってみた。
RRH
使い方は簡単でcreateStore
をhookにした感じ
Provider側
import React from 'react'; import { useProvider } from 'rrh'; import reducer from './reducer'; import middleware from './middleware' import Child from './child'; const Component = () => { const Provider = useProvider(() => ({ reducer, preloadedState: {count: 1}, storeEnhancer: applyMiddleware(middleware) })); return <Provider><Child></Provider> }
connectもhookにしてみた
import React from 'react'; import { useSelector } from 'rrh'; const Child = (props) => { const selected = useSelector( props, (dispatch, props) => ({ increment: () => dispatch({type: 'INCREMENT', payload: 1}), decrement: () => dispatch({type: 'DECREMENT', payload: -1}), }), (state, props) => ({count: state.count}), state => [state.count] ); return ( <div> {selected.count} <button onClick={selected.increment}>INC</button> <button onClick={selected.decrement}>DEC</button> </div> ) }
使い勝手は素のreduxに近くなるようにしてみた。
typescript
tsで作ってるので、型定義も同包してます。
WeJS2周年おめでとうございます
久しぶりのブログはこちらのアドベントカレンダー
WeJSについて
We are Javascritpers
初心者登壇歓迎なJavascript勉強会
最近はJS初心者歓迎も兼ねていて、多方面で参加しやすい勉強会になってる
思い出
21Cafeでやってるときに参加して依頼、だいたい行ってると思う
3回目くらいからかな?
全ての勉強会の中でここで最もLTしてると思う
いつもお世話になっております
ここでいろんな人と知り合いになれました
良いなと思っていること
今の時代にjavascriptで初心者歓迎なLT会は貴重
とにかく登壇の敷居を下げてくれているので登壇しやすい
特に一度も登壇したことない人はぜひ申し込んで登壇してほしい
ただ、倍率が高いので抽選通るまで頑張ろう
あと毎回賑やかでよい
懇親会とかすごく話しやすい雰囲気だなぁと毎回思う
これもひとえに主催者の方々の人柄ですね
これからWeJSでLTする方へ
自分のネタがしょぼいかなとか、みんな知ってるしとか考えなくていいよ!
まずは自分が楽しもう
昔WeJSでLTした心得的なやつ
最後に
いつもありがとうございます。毎月楽しみにしてます。感謝。
JSのproposal-numeric-separatorを実装したよ
タイトルの通りなんだけど、書くのが遅れてしまった。
V8 のmasterにマージされた。
機能
javascriptのプロポーザルで数値リテラルの間にアンダースコアを挿入できるやつ
現在stage3にあってまだ正式に採用されているわけではない。
このスライドにも書いた
例.
1_0_00_0 0xFF_FF_FF 0b0101_01_01 0o7_7_7
16進数、10進数、8進数、2進数の各リテラルに対応している。
あとBigIntにも当然対応している
39_950_934n
ただ、実装したあとに修正してもらったんだけど、implicit octalには対応していない。
0777_7 // Error!
あと、以下の場合はすべてエラー
0x_000 // Error ! 0b_01 // Error ! 0o_10 // Error !
アンダースコアの重複と末尾もだめ
12__00 // Error ! 1200_ // Error !
制限
キャストされる数値には使えません!
Number('1_0_0') // Error ! parseInt('1_0_0', 10) // Error !
以下のIssueに詳しい
簡単に話すと今までは
IsNaN(Number('1_0_0')) // true
だったので急にアンダースコアを数値として使えるようになると、NaN
であることを期待しているコードが壊れるからという理由。
そのようなコードが一体どれほどあるのか知らないけど安全側に倒した。
breaking webを避けるためには仕方ないかなと言う感じ。
以上です。