Skip to content

Commit 63b5eda

Browse files
authored
Merge pull request #641 from gnieh/json/render
Improve stream rendering performances
2 parents e3e6561 + 6d74393 commit 63b5eda

File tree

6 files changed

+425
-172
lines changed

6 files changed

+425
-172
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package fs2.data.benchmarks
2+
3+
import cats.effect.SyncIO
4+
import cats.effect.IO
5+
import fs2.data.json.Token
6+
import fs2.data.json.circe.*
7+
import fs2.{Fallible, Stream}
8+
import io.circe.Json
9+
import org.openjdk.jmh.annotations.*
10+
import org.openjdk.jmh.infra.Blackhole
11+
import cats.effect.unsafe.implicits.global
12+
13+
import java.util.concurrent.TimeUnit
14+
15+
@OutputTimeUnit(TimeUnit.MICROSECONDS)
16+
@BenchmarkMode(Array(Mode.AverageTime))
17+
@State(org.openjdk.jmh.annotations.Scope.Benchmark)
18+
@Fork(value = 1)
19+
@Warmup(iterations = 15, time = 5)
20+
@Measurement(iterations = 10, time = 5)
21+
class PrinterBenchmarks {
22+
23+
val intArrayStream =
24+
Stream.emits(
25+
Token.StartArray ::
26+
(List
27+
.range(0, 1000000)
28+
.map(i => Token.NumberValue(i.toString())) :+ Token.EndArray))
29+
30+
val objectStream =
31+
Stream.emits(
32+
Token.StartObject ::
33+
(List
34+
.range(0, 1000000)
35+
.flatMap(i => List(Token.Key(s"key:$i"), Token.NumberValue(i.toString()))) :+ Token.EndObject))
36+
37+
@Benchmark
38+
def intArrayCompact(bh: Blackhole) =
39+
bh.consume(
40+
intArrayStream
41+
.through(fs2.data.json.render.compact)
42+
.compile
43+
.drain)
44+
45+
@Benchmark
46+
def objectCompact(bh: Blackhole) =
47+
bh.consume(
48+
objectStream
49+
.through(fs2.data.json.render.compact)
50+
.compile
51+
.drain)
52+
53+
@Benchmark
54+
def intArrayPretty(bh: Blackhole) =
55+
bh.consume(
56+
intArrayStream
57+
.through(fs2.data.json.render.prettyPrint())
58+
.compile
59+
.drain)
60+
61+
@Benchmark
62+
def objectPretty(bh: Blackhole) =
63+
bh.consume(
64+
objectStream
65+
.through(fs2.data.json.render.prettyPrint())
66+
.compile
67+
.drain)
68+
69+
@Benchmark
70+
def intArrayPrettyLegacy(bh: Blackhole) =
71+
bh.consume(
72+
intArrayStream
73+
.through(fs2.data.json.render.pretty())
74+
.compile
75+
.drain)
76+
77+
@Benchmark
78+
def objectPrettyLegacy(bh: Blackhole) =
79+
bh.consume(
80+
objectStream
81+
.through(fs2.data.json.render.pretty())
82+
.compile
83+
.drain)
84+
85+
}

build.sbt

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,46 @@ lazy val text = crossProject(JVMPlatform, JSPlatform, NativePlatform)
142142
ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.data.text.CharLikeStringChunks.pullNext"),
143143
ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.data.text.CharLikeStringChunks.advance"),
144144
ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.data.text.CharLikeStringChunks.current"),
145-
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.CharLikeStringChunks$StringContext")
145+
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.CharLikeStringChunks$StringContext"),
146+
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$AlignBegin"),
147+
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$AlignBegin$"),
148+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignBegin.apply"),
149+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignBegin.unapply"),
150+
ProblemFilters.exclude[IncompatibleResultTypeProblem](
151+
"fs2.data.text.render.internal.Annotated#AlignBegin.fromProduct"),
152+
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$AlignEnd"),
153+
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$AlignEnd$"),
154+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignEnd.apply"),
155+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignEnd.unapply"),
156+
ProblemFilters.exclude[IncompatibleResultTypeProblem](
157+
"fs2.data.text.render.internal.Annotated#AlignEnd.fromProduct"),
158+
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$GroupEnd"),
159+
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$GroupEnd$"),
160+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#GroupEnd.apply"),
161+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#GroupEnd.unapply"),
162+
ProblemFilters.exclude[IncompatibleResultTypeProblem](
163+
"fs2.data.text.render.internal.Annotated#GroupEnd.fromProduct"),
164+
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$IndentBegin"),
165+
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$IndentBegin$"),
166+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentBegin.apply"),
167+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentBegin.unapply"),
168+
ProblemFilters.exclude[IncompatibleResultTypeProblem](
169+
"fs2.data.text.render.internal.Annotated#IndentBegin.fromProduct"),
170+
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$IndentEnd"),
171+
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$IndentEnd$"),
172+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentEnd.apply"),
173+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentEnd.unapply"),
174+
ProblemFilters.exclude[IncompatibleResultTypeProblem](
175+
"fs2.data.text.render.internal.Annotated#IndentEnd.fromProduct"),
176+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Line.hp"),
177+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#LineBreak.hp"),
178+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.hp"),
179+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.copy"),
180+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.copy$default$2"),
181+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.this"),
182+
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$Text$"),
183+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.apply"),
184+
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text._2")
146185
)
147186
)
148187
.nativeSettings(

json/src/main/scala/fs2/data/json/package.scala

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,71 @@ package object json {
154154
* You can use this to write the Json stream to a file.
155155
*/
156156
def compact[F[_]]: Pipe[F, Token, String] =
157-
_.through(fs2.data.text.render.pretty(width = Int.MaxValue)(Token.compact))
157+
_.scanChunks((0, false)) { case (state, chunk) =>
158+
val builder = new StringBuilder
159+
val state1 =
160+
chunk.foldLeft(state) {
161+
case ((level, comma), Token.StartObject) =>
162+
if (comma) {
163+
builder.append(',')
164+
}
165+
builder.append(('{'))
166+
(level + 1, false)
167+
case ((level, _), Token.EndObject) =>
168+
builder.append('}')
169+
(level - 1, level > 1)
170+
case ((level, comma), Token.StartArray) =>
171+
if (comma) {
172+
builder.append(',')
173+
}
174+
builder.append(('['))
175+
(level + 1, false)
176+
case ((level, _), Token.EndArray) =>
177+
builder.append(']')
178+
(level - 1, level > 1)
179+
case ((level, comma), Token.Key(key)) =>
180+
if (comma) {
181+
builder.append(',')
182+
}
183+
builder.append('"')
184+
Token.renderString(key, 0, builder)
185+
builder.append("\":")
186+
(level, false)
187+
case ((level, comma), Token.StringValue(key)) =>
188+
if (comma) {
189+
builder.append(',')
190+
}
191+
builder.append('"')
192+
Token.renderString(key, 0, builder)
193+
builder.append('"')
194+
(level, level > 0)
195+
case ((level, comma), Token.NumberValue(n)) =>
196+
if (comma) {
197+
builder.append(',')
198+
}
199+
builder.append(n)
200+
(level, level > 0)
201+
case ((level, comma), Token.TrueValue) =>
202+
if (comma) {
203+
builder.append(',')
204+
}
205+
builder.append("true")
206+
(level, level > 0)
207+
case ((level, comma), Token.FalseValue) =>
208+
if (comma) {
209+
builder.append(',')
210+
}
211+
builder.append("false")
212+
(level, level > 0)
213+
case ((level, comma), Token.NullValue) =>
214+
if (comma) {
215+
builder.append(',')
216+
}
217+
builder.append("null")
218+
(level, level > 0)
219+
}
220+
(state1, Chunk.singleton(builder.result()))
221+
}
158222

159223
/** Renders a pretty-printed representation of the token stream with the given
160224
* indentation size.

text/shared/src/main/scala/fs2/data/text/render/internal/Annotated.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ package fs2.data.text.render.internal
1818

1919
private sealed trait Annotated
2020
private object Annotated {
21-
case class Text(text: String, hp: Int) extends Annotated
22-
case class Line(hp: Int) extends Annotated
23-
case class LineBreak(hp: Int) extends Annotated
21+
case class Text(text: String) extends Annotated
22+
case class Line(pos: Int) extends Annotated
23+
case class LineBreak(pos: Int) extends Annotated
2424
case class GroupBegin(hpl: Position) extends Annotated
25-
case class GroupEnd(hp: Int) extends Annotated
26-
case class IndentBegin(hp: Int) extends Annotated
27-
case class IndentEnd(hp: Int) extends Annotated
28-
case class AlignBegin(hp: Int) extends Annotated
29-
case class AlignEnd(hp: Int) extends Annotated
25+
case object GroupEnd extends Annotated
26+
case object IndentBegin extends Annotated
27+
case object IndentEnd extends Annotated
28+
case object AlignBegin extends Annotated
29+
case object AlignEnd extends Annotated
3030
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2024 fs2-data Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fs2.data.text.render.internal
18+
19+
private sealed trait NonEmptyIntList {
20+
def head: Int
21+
def ::(i: Int): NonEmptyIntList =
22+
More(i, this)
23+
def incHead: NonEmptyIntList
24+
def decHead: NonEmptyIntList
25+
def pop: NonEmptyIntList
26+
}
27+
private final case class One(head: Int) extends NonEmptyIntList {
28+
override def incHead: NonEmptyIntList = One(head + 1)
29+
override def decHead: NonEmptyIntList = One(head - 1)
30+
override lazy val pop: NonEmptyIntList = One(0)
31+
}
32+
private final case class More(head: Int, tail: NonEmptyIntList) extends NonEmptyIntList {
33+
override def incHead: NonEmptyIntList = More(head + 1, tail)
34+
override def decHead: NonEmptyIntList = More(head - 1, tail)
35+
override def pop: NonEmptyIntList = tail
36+
}

0 commit comments

Comments
 (0)