Crystal: to_i64 takes very long to release-compile

Created on 3 Aug 2020  路  22Comments  路  Source: crystal-lang/crystal

  • Environment:
$ crystal -v
Crystal 0.35.1 [5999ae29b] (2020-06-19)

LLVM: 8.0.0
Default target: x86_64-unknown-linux-gnu
  • Compile option: time crystal build --release --no-debug

  • Observed:

gets.not_nil!.to_i

takes around 11 seconds to compile. But

gets.not_nil!.to_i64

takes around 33 seconds to compile. I think previously this was not happening.

  • Expected:

The latter (to_i64) should also compile similar time (to_i) to the former.

All 22 comments

--stats option describes which part of compilation takes time:

[momonga@elma ~ x86_64]$ crystal eval --release --stats 'gets.not_nil!.to_i64'
Parse:                             00:00:00.000073166 (   0.25MB)
Semantic (top level):              00:00:00.692509122 (  45.54MB)
Semantic (new):                    00:00:00.002251068 (  45.54MB)
Semantic (type declarations):      00:00:00.055071657 (  45.54MB)
Semantic (abstract def check):     00:00:00.014118366 (  45.54MB)
Semantic (ivars initializers):     00:00:00.019376271 (  45.54MB)
Semantic (cvars initializers):     00:00:00.290701988 (  76.72MB)
Semantic (main):                   00:00:00.127858996 (  76.72MB)
Semantic (cleanup):                00:00:00.015213651 (  76.72MB)
Semantic (recursive struct check): 00:00:00.002209866 (  76.72MB)
Codegen (crystal):                 00:00:00.389143089 (  76.78MB)
Codegen (bc+obj):                  00:00:10.157504508 (  76.78MB)
Codegen (linking):                 00:00:00.077172808 (  76.78MB)

Codegen (bc+obj):
 - no previous .o files were reused
100
[momonga@elma ~ x86_64]$ crystal eval --release --stats 'gets.not_nil!.to_i'
Parse:                             00:00:00.000078505 (   0.25MB)
Semantic (top level):              00:00:00.664702044 (  45.54MB)
Semantic (new):                    00:00:00.002223303 (  45.54MB)
Semantic (type declarations):      00:00:00.053093154 (  45.54MB)
Semantic (abstract def check):     00:00:00.015838700 (  45.54MB)
Semantic (ivars initializers):     00:00:00.018744152 (  45.54MB)
Semantic (cvars initializers):     00:00:00.270165809 (  76.72MB)
Semantic (main):                   00:00:00.122609172 (  76.72MB)
Semantic (cleanup):                00:00:00.014394756 (  76.72MB)
Semantic (recursive struct check): 00:00:00.002078072 (  76.72MB)
Codegen (crystal):                 00:00:00.369789819 (  76.78MB)
Codegen (bc+obj):                  00:00:06.535286662 (  76.78MB)
Codegen (linking):                 00:00:00.075627428 (  76.78MB)

Codegen (bc+obj):
 - no previous .o files were reused
100
[momonga@elma ~ x86_64]$ crystal -v
Crystal 0.35.1 ()

LLVM: 10.0.0
Default target: x86_64-unknown-linux-gnu

I cannot reproduce this issue. The timings could be affected by a variety of factors. Make sure to clean the cache (~/.cache/crystal) before measuring.

I have this stats:

$ rm -rf ~/.cache/crystal && crystal eval --release --stats 'gets.not_nil!.to_i'
Parse:                             00:00:00.000033321 (   0.25MB)
Semantic (top level):              00:00:00.284489562 (  45.54MB)
Semantic (new):                    00:00:00.001281893 (  45.54MB)
Semantic (type declarations):      00:00:00.017540037 (  45.54MB)
Semantic (abstract def check):     00:00:00.005079098 (  45.54MB)
Semantic (ivars initializers):     00:00:00.005611963 (  45.54MB)
Semantic (cvars initializers):     00:00:00.092519776 (  76.72MB)
Semantic (main):                   00:00:00.038549817 (  76.72MB)
Semantic (cleanup):                00:00:00.004147029 (  76.72MB)
Semantic (recursive struct check): 00:00:00.000683779 (  76.72MB)
Codegen (crystal):                 00:00:00.381705459 (  76.78MB)
Codegen (bc+obj):                  00:00:11.464713278 (  76.78MB)
Codegen (linking):                 00:00:00.142163408 (  76.78MB)

Codegen (bc+obj):
 - no previous .o files were reused
1
$ rm -rf ~/.cache/crystal && crystal eval --release --stats 'gets.not_nil!.to_i64'
Parse:                             00:00:00.000023485 (   0.25MB)
Semantic (top level):              00:00:00.227755626 (  45.54MB)
Semantic (new):                    00:00:00.001445818 (  45.54MB)
Semantic (type declarations):      00:00:00.016583849 (  45.54MB)
Semantic (abstract def check):     00:00:00.004330381 (  45.54MB)
Semantic (ivars initializers):     00:00:00.005551524 (  45.54MB)
Semantic (cvars initializers):     00:00:00.095030432 (  76.72MB)
Semantic (main):                   00:00:00.042350488 (  76.72MB)
Semantic (cleanup):                00:00:00.004409815 (  76.72MB)
Semantic (recursive struct check): 00:00:00.000779521 (  76.72MB)
Codegen (crystal):                 00:00:00.373667444 (  76.78MB)
Codegen (bc+obj):                  00:00:33.554118575 (  76.78MB)
Codegen (linking):                 00:00:00.103622910 (  76.78MB)

Codegen (bc+obj):
 - no previous .o files were reused
1

The bc+obj time for @lugia-kun is more or less twice the time for to_i64 compared to to_i. The magnitude is similar for @cielavenir . Maybe your system is just slow, what's your CPU?

It can be poorly optimized for the officially distributed compiler: crystal-0.35.1-1-linux-x86_64.tar.gz

[momonga@elma ~ x86_64]$ rm -rf .cache/crystal && ./crystal-0.35.1-1/bin/crystal eval --release --stats 'gets.not_nil!.to_i64' </dev/null
Parse:                             00:00:00.000023906 (   0.25MB)
Semantic (top level):              00:00:00.213184473 (  45.54MB)
Semantic (new):                    00:00:00.001222640 (  45.54MB)
Semantic (type declarations):      00:00:00.016582822 (  45.54MB)
Semantic (abstract def check):     00:00:00.003959927 (  45.54MB)
Semantic (ivars initializers):     00:00:00.006416105 (  45.54MB)
Semantic (cvars initializers):     00:00:00.096440364 (  76.72MB)
Semantic (main):                   00:00:00.043317692 (  76.72MB)
Semantic (cleanup):                00:00:00.005042655 (  76.72MB)
Semantic (recursive struct check): 00:00:00.000791469 (  76.72MB)
Codegen (crystal):                 00:00:00.339030335 (  76.78MB)
Codegen (bc+obj):                  00:00:25.886882478 (  76.78MB)
Codegen (linking):                 00:00:00.081542084 (  76.78MB)

Codegen (bc+obj):
 - no previous .o files were reused
Unhandled exception: Nil assertion failed (NilAssertionError)
Error while trying to dump the backtrace: Arithmetic overflow (OverflowError)
[momonga@elma ~ x86_64]$ rm -rf .cache/crystal && ./crystal-0.35.1-1/bin/crystal eval --release --stats 'gets.not_nil!.to_i' </dev/null
Parse:                             00:00:00.000024877 (   0.25MB)
Semantic (top level):              00:00:00.201950076 (  45.54MB)
Semantic (new):                    00:00:00.001416522 (  45.54MB)
Semantic (type declarations):      00:00:00.016278520 (  45.54MB)
Semantic (abstract def check):     00:00:00.004202345 (  45.54MB)
Semantic (ivars initializers):     00:00:00.006098250 (  45.54MB)
Semantic (cvars initializers):     00:00:00.095066752 (  76.72MB)
Semantic (main):                   00:00:00.044211634 (  76.72MB)
Semantic (cleanup):                00:00:00.005056823 (  76.72MB)
Semantic (recursive struct check): 00:00:00.000728720 (  76.72MB)
Codegen (crystal):                 00:00:00.328815816 (  76.78MB)
Codegen (bc+obj):                  00:00:09.226124645 (  76.78MB)
Codegen (linking):                 00:00:00.087035636 (  76.78MB)

Codegen (bc+obj):
 - no previous .o files were reused
Unhandled exception: Nil assertion failed (NilAssertionError)
Error while trying to dump the backtrace: Arithmetic overflow (OverflowError)
[momonga@elma ~ x86_64]$ ./crystal-0.35.1-1/bin/crystal -v
Crystal 0.35.1 [5999ae29b] (2020-06-19)

LLVM: 8.0.0
Default target: x86_64-unknown-linux-gnu

I have investigated the built binaries. The compiler instantiated more methods and constants, and 3 symbols left not inlined:

$ diff -u <(nm to_i.o | cut -c 18-) <(nm to_i64.o | cut -c 18-)
--- /dev/fd/63  2020-08-04 20:27:22.994397405 +0900
+++ /dev/fd/62  2020-08-04 20:27:22.994397405 +0900
@@ -142,6 +142,7 @@
 t *String#inspect_char<Char, (UInt8 | Nil), String::Builder>:(String::Builder | Nil)
 t *String#starts_with?<Char>:Bool
 t *String#to_i:Int32
+t *String#to_u64_info<Int32, Bool, Bool, Bool, Bool, Bool, Bool>:String::ToU64Info
 t *String#underscore:String
 t *String::Builder#to_s:String
 t *String::Builder#write<Slice(UInt8)>:Nil
@@ -154,6 +155,7 @@
 t *String::interpolation<String, (Bool.class | Int32.class | Int64.class | Nil.class | Slice(UInt8).class | String.class | UInt16.class | UInt32.class | UInt64.class | UInt8.class), String>:String
 t *String::interpolation<String, (Bool.class | Int32.class | Int64.class | Slice(UInt8).class | String.class | UInt16.class | UInt32.class | UInt64.class | UInt8.class), String>:String
 t *String::interpolation<String, (String | Nil), String>:String
+t *String::interpolation<String, Int32>:String
 t *String::interpolation<String, Signal>:String
 t *String::interpolation<String, String, String, String>:String
 t *String::interpolation<String, String, String>:String
@@ -339,6 +341,7 @@
 b Signal::sigset
 b Signal::sigset:init
 b String::CHAR_TO_DIGIT
+b String::CHAR_TO_DIGIT62
 b String::CHAR_TO_DIGIT62:init
 b String::CHAR_TO_DIGIT:init
 b String::TYPE_ID:init

So, it can be normal to take more time to compile the to_i64 version.

It's funny because both methods rely on more or less the same underlying methods. So when you do to_i it actually calls something like to_i64.to_i. Just so you know :-)

On my machine it's 6s for to_i vs 9s for to_i64.

That said, the compiler is doing what it needs to do. Optimizing these things is mostly impossible. The time is spent in the bc+obj phase which is basically LLVM. We don't control what LLVM does there.

What we can do is generate more efficient LLVM code, and that will affect all things more or less equally. But if to_i64 needs to do more stuff compared to to_i, then it'll be slower to compile.

Another thing: if you compile an empty program, it seems String#to_i is already compiled by default. You can check this with this diff and then using bin/crystal:

diff --git a/src/string.cr b/src/string.cr
index e9b70b1e3..e9b1cf967 100644
--- a/src/string.cr
+++ b/src/string.cr
@@ -515,6 +515,7 @@ class String
       {% end %}
       info.value.to_{{method}}
     end
+    {% puts method %}
   end

   private def to_u64_info(base, whitespace, underscore, prefix, strict, leading_zero_is_octal, unsigned)

So when you write:

gets.not_nil!.to_i64

this is actually compiling to_i and to_i64. When you do:

gets.not_nil!.to_i

it's just compiling to_i.

In essence, we are comparing the time of doing nothing vs. the time of compiling to_i64. Of course there will be a significant difference!

I suggest to close this issue.

@j8r my CPU is Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz

The original issue was reported to me by @yuki2006 .

His CPU is Intel(R) Xeon(R) CPU @ 2.30GHz and crystal is:

Crystal 0.35.1 [5999ae29b] (2020-06-19)
LLVM: 8.0.0
Default target: x86_64-unknown-linux-gnu

stat:

  • to_i
Parse:                             00:00:00.000032629 (   0.25MB)
Semantic (top level):              00:00:00.291828662 (  44.45MB)
Semantic (new):                    00:00:00.001687647 (  44.45MB)
Semantic (type declarations):      00:00:00.021765071 (  44.45MB)
Semantic (abstract def check):     00:00:00.004676018 (  44.45MB)
Semantic (ivars initializers):     00:00:00.070696369 (  44.45MB)
Semantic (cvars initializers):     00:00:00.082283203 (  75.26MB)
Semantic (main):                   00:00:00.047436322 (  75.26MB)
Semantic (cleanup):                00:00:00.006083323 (  75.26MB)
Semantic (recursive struct check): 00:00:00.000825238 (  75.26MB)
Codegen (crystal):                 00:00:00.453231645 (  91.32MB)
Codegen (bc+obj):                  00:00:10.657633725 (  91.32MB)
Codegen (linking):                 00:00:00.083715307 (  91.32MB)
Codegen (bc+obj):
 - no previous .o files were reused
  • to_i64
Parse:                             00:00:00.000031968 (   0.25MB)
Semantic (top level):              00:00:00.294110739 (  44.45MB)
Semantic (new):                    00:00:00.001677529 (  44.45MB)
Semantic (type declarations):      00:00:00.021939479 (  44.45MB)
Semantic (abstract def check):     00:00:00.004600512 (  44.45MB)
Semantic (ivars initializers):     00:00:00.070553723 (  44.45MB)
Semantic (cvars initializers):     00:00:00.084688932 (  75.26MB)
Semantic (main):                   00:00:00.048132542 (  75.26MB)
Semantic (cleanup):                00:00:00.006218824 (  75.26MB)
Semantic (recursive struct check): 00:00:00.000831249 (  75.26MB)
Codegen (crystal):                 00:00:00.453403316 (  91.32MB)
Codegen (bc+obj):                  00:00:30.041210954 (  91.32MB)
Codegen (linking):                 00:00:00.078673201 (  91.32MB)
Codegen (bc+obj):
 - no previous .o files were reused

But the story does not finish. He has old disk snapshot, which has Crystal 0.33.0 [612825a53] (2020-02-14).

  • to_i
Parse:                             00:00:00.000023330 (   0.25MB)
Semantic (top level):              00:00:01.559084943 (  45.82MB)
Semantic (new):                    00:00:00.001092173 (  45.82MB)
Semantic (type declarations):      00:00:00.019076632 (  45.82MB)
Semantic (abstract def check):     00:00:00.004930274 (  45.82MB)
Semantic (ivars initializers):     00:00:00.005639646 (  45.82MB)
Semantic (cvars initializers):     00:00:00.131596631 (  61.10MB)
Semantic (main):                   00:00:00.042042104 (  77.10MB)
Semantic (cleanup):                00:00:00.005940622 (  77.10MB)
Semantic (recursive struct check): 00:00:00.000603709 (  77.10MB)
Codegen (crystal):                 00:00:00.412113621 (  77.16MB)
Codegen (bc+obj):                  00:00:08.906839599 (  77.16MB)
/usr/bin/ld: skipping incompatible /usr/lib/libm.so when searching for -lm
/usr/bin/ld: skipping incompatible /usr/lib/libpthread.so when searching for -lpthread
/usr/bin/ld: skipping incompatible /usr/lib/librt.so when searching for -lrt
/usr/bin/ld: skipping incompatible /usr/lib/libdl.so when searching for -ldl
/usr/bin/ld: skipping incompatible /usr/lib/libc.so when searching for -lc
Codegen (linking):                 00:00:02.185809997 (  77.16MB)
Codegen (bc+obj):
 - no previous .o files were reused
  • to_i64
Parse:                             00:00:00.000030702 (   0.25MB)
Semantic (top level):              00:00:00.276066559 (  45.99MB)
Semantic (new):                    00:00:00.001086318 (  45.99MB)
Semantic (type declarations):      00:00:00.019087112 (  45.99MB)
Semantic (abstract def check):     00:00:00.003854486 (  45.99MB)
Semantic (ivars initializers):     00:00:00.005610338 (  45.99MB)
Semantic (cvars initializers):     00:00:00.129068388 (  61.32MB)
Semantic (main):                   00:00:00.041045604 (  77.32MB)
Semantic (cleanup):                00:00:00.005255247 (  77.32MB)
Semantic (recursive struct check): 00:00:00.000704764 (  77.32MB)
Codegen (crystal):                 00:00:00.403326771 (  77.38MB)
Codegen (bc+obj):                  00:00:08.916179773 (  77.38MB)
/usr/bin/ld: skipping incompatible /usr/lib/libm.so when searching for -lm
/usr/bin/ld: skipping incompatible /usr/lib/libpthread.so when searching for -lpthread
/usr/bin/ld: skipping incompatible /usr/lib/librt.so when searching for -lrt
/usr/bin/ld: skipping incompatible /usr/lib/libdl.so when searching for -ldl
/usr/bin/ld: skipping incompatible /usr/lib/libc.so when searching for -lc
Codegen (linking):                 00:00:00.085900883 (  77.38MB)
Codegen (bc+obj):
 - no previous .o files were reused

You can see that in 0.33.0 bc+obj time does not change among to_i and to_i64. Could you explain?

FWIW Compile times for this (and other things I imagine) have been getting slower over time.

0.25.0 to 0.30.0

docker run -it crystallang/crystal:0.25.0 crystal eval --release --stats --progress 'gets.not_nil!.to_i64'
Parse:                             00:00:00.000035786 (   0.25MB)
Semantic (top level):              00:00:00.107019171 (  32.83MB)
Semantic (new):                    00:00:00.001337179 (  32.83MB)
Semantic (type declarations):      00:00:00.011553308 (  32.83MB)
Semantic (abstract def check):     00:00:00.001130660 (  32.83MB)
Semantic (ivars initializers):     00:00:00.000878728 (  32.83MB)
Semantic (cvars initializers):     00:00:00.088217028 (  58.45MB)
Semantic (main):                   00:00:00.019033643 (  58.45MB)
Semantic (cleanup):                00:00:00.002707035 (  58.45MB)
Semantic (recursive struct check): 00:00:00.000723621 (  58.45MB)
Codegen (crystal):                 00:00:00.105389099 (  74.45MB)
Codegen (bc+obj):                  00:00:03.349342286 (  74.45MB)
Codegen (linking):                 00:00:00.049109274 (  74.45MB)

Codegen (bc+obj):
 - no previous .o files were reused

0.31.0 to 0.34.0

docker run -it crystallang/crystal:0.34.0 crystal eval --release --stats --progress 'gets.not_nil!.to_i64'
Parse:                             00:00:00.000021629 (   0.25MB)
Semantic (top level):              00:00:00.164409635 (  45.54MB)
Semantic (new):                    00:00:00.001612343 (  45.54MB)
Semantic (type declarations):      00:00:00.014134920 (  45.54MB)
Semantic (abstract def check):     00:00:00.003582688 (  45.54MB)
Semantic (ivars initializers):     00:00:00.004992114 (  45.54MB)
Semantic (cvars initializers):     00:00:00.071873382 (  76.72MB)
Semantic (main):                   00:00:00.032214647 (  76.72MB)
Semantic (cleanup):                00:00:00.003966003 (  76.72MB)
Semantic (recursive struct check): 00:00:00.000790823 (  76.72MB)
Codegen (crystal):                 00:00:00.256457828 (  76.78MB)
Codegen (bc+obj):                  00:00:06.584822815 (  76.78MB)
Codegen (linking):                 00:00:00.062545097 (  76.78MB)

Codegen (bc+obj):
 - no previous .o files were reused

0.35.0

docker run -it crystallang/crystal:0.35.0 crystal eval --release --stats --progress 'gets.not_nil!.to_i64'
Parse:                             00:00:00.000022659 (   0.25MB)
Semantic (top level):              00:00:00.173586168 (  44.92MB)
Semantic (new):                    00:00:00.001235234 (  44.92MB)
Semantic (type declarations):      00:00:00.012906685 (  44.92MB)
Semantic (abstract def check):     00:00:00.003277784 (  44.92MB)
Semantic (ivars initializers):     00:00:00.004992250 (  44.92MB)
Semantic (cvars initializers):     00:00:00.069826035 (  75.89MB)
Semantic (main):                   00:00:00.032201420 (  75.89MB)
Semantic (cleanup):                00:00:00.003961809 (  75.89MB)
Semantic (recursive struct check): 00:00:00.000684551 (  75.89MB)
Codegen (crystal):                 00:00:00.284120471 (  91.95MB)
Codegen (bc+obj):                  00:00:22.515643632 (  91.95MB)
Codegen (linking):                 00:00:00.061558175 (  91.95MB)

Codegen (bc+obj):
 - no previous .o files were reused

Probably would have to bisect to see what actually is causing it tho.

FWIW Compile times for this (and other things I imagine) have been getting slower over time.

Of course, the runtime is getting bigger and bigger.

Again, guys, you are comparing to_i vs. to_i64 when in reality you are comparing void to to_i64. You are doing the wrong comparison.

Of course, the runtime is getting bigger and bigger.

I could see a few seconds here and there, but ~7x longer? That's a bit extreme don't you think?

Try --single-module --emit llvm-ir with both versions, then compare the size of the generated .ll files and let me know the difference (I don't have time to try it right now).

Also, LLVM gets slower and slower every release. What LLVM is used in your case for 0.25.0 compared to 0.35.0?

Note that the time that goes up is mostly bc+obj: that's LLVM. The other stages take more or less the same time.

Again, it's an LLVM problem, not ours. There's nothing we can do about it. And people here are comparing the wrong stuff.

@asterite Let's treat it as an inquiry into what could be done, if anything, to keep the compilation time down.

then compare the size of the generated .ll files

  • 0.30.0 - 4.2M, 58624 lines
  • 0.31.0 - 5.7M, 82254 lines
  • 0.35.0 - 6.5M, 96216 lines

EDIT: Added lines

As @lugia-kun firstly tested LLVM10, I compiled crystal from source-code and did the same benchmark:

$ bin/crystal -v
Using compiled compiler at .build/crystal
Crystal 0.35.1 [5999ae29b] (2020-06-19)
LLVM: 10.0.1
Default target: x86_64-pc-linux-gnu
$ rm -rf ~/.cache/crystal && bin/crystal eval --release --stats 'gets.not_nil!.to_i'
Using compiled compiler at .build/crystal
Parse:                             00:00:00.000713366 (   0.25MB)
Semantic (top level):              00:00:00.241455265 (  46.21MB)
Semantic (new):                    00:00:00.002034125 (  46.21MB)
Semantic (type declarations):      00:00:00.027162018 (  46.21MB)
Semantic (abstract def check):     00:00:00.013913697 (  46.21MB)
Semantic (ivars initializers):     00:00:00.018292923 (  46.21MB)
Semantic (cvars initializers):     00:00:00.076908969 (  77.61MB)
Semantic (main):                   00:00:00.043314501 (  77.61MB)
Semantic (cleanup):                00:00:00.004746504 (  77.61MB)
Semantic (recursive struct check): 00:00:00.000637040 (  77.61MB)
Codegen (crystal):                 00:00:00.284896732 (  77.67MB)
Codegen (bc+obj):                  00:00:08.746529122 (  77.67MB)
Codegen (linking):                 00:00:00.167106955 (  77.67MB)
Codegen (bc+obj):
 - no previous .o files were reused
1
$ rm -rf ~/.cache/crystal && bin/crystal eval --release --stats 'gets.not_nil!.to_i64'
Using compiled compiler at .build/crystal
Parse:                             00:00:00.000040826 (   0.25MB)
Semantic (top level):              00:00:00.211791447 (  45.12MB)
Semantic (new):                    00:00:00.001434362 (  45.12MB)
Semantic (type declarations):      00:00:00.026321564 (  45.12MB)
Semantic (abstract def check):     00:00:00.003773349 (  45.12MB)
Semantic (ivars initializers):     00:00:00.004922256 (  45.12MB)
Semantic (cvars initializers):     00:00:00.077086637 (  76.16MB)
Semantic (main):                   00:00:00.041165638 (  76.16MB)
Semantic (cleanup):                00:00:00.004918474 (  76.16MB)
Semantic (recursive struct check): 00:00:00.000776782 (  76.16MB)
Codegen (crystal):                 00:00:00.318335552 (  92.22MB)
Codegen (bc+obj):                  00:00:14.962540070 (  92.22MB)
Codegen (linking):                 00:00:00.163606497 (  92.22MB)
Codegen (bc+obj):
 - no previous .o files were reused
1

sorry (bad keyboard...)

well, is it possible to upgrade LLVM in official distribution? with LLVM10, the issue is less affected.

Was this page helpful?
0 / 5 - 0 ratings