Skip to content

Commit

Permalink
Merge pull request #646 from gnieh/json/render
Browse files Browse the repository at this point in the history
Backporting #641 to main branch
  • Loading branch information
satabin authored Dec 23, 2024
2 parents 89ab656 + 8358065 commit 5baf371
Show file tree
Hide file tree
Showing 6 changed files with 425 additions and 173 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package fs2.data.benchmarks

import cats.effect.SyncIO
import cats.effect.IO
import fs2.data.json.Token
import fs2.data.json.circe.*
import fs2.{Fallible, Stream}
import io.circe.Json
import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.infra.Blackhole
import cats.effect.unsafe.implicits.global

import java.util.concurrent.TimeUnit

@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Array(Mode.AverageTime))
@State(org.openjdk.jmh.annotations.Scope.Benchmark)
@Fork(value = 1)
@Warmup(iterations = 15, time = 5)
@Measurement(iterations = 10, time = 5)
class PrinterBenchmarks {

val intArrayStream =
Stream.emits(
Token.StartArray ::
(List
.range(0, 1000000)
.map(i => Token.NumberValue(i.toString())) :+ Token.EndArray))

val objectStream =
Stream.emits(
Token.StartObject ::
(List
.range(0, 1000000)
.flatMap(i => List(Token.Key(s"key:$i"), Token.NumberValue(i.toString()))) :+ Token.EndObject))

@Benchmark
def intArrayCompact(bh: Blackhole) =
bh.consume(
intArrayStream
.through(fs2.data.json.render.compact)
.compile
.drain)

@Benchmark
def objectCompact(bh: Blackhole) =
bh.consume(
objectStream
.through(fs2.data.json.render.compact)
.compile
.drain)

@Benchmark
def intArrayPretty(bh: Blackhole) =
bh.consume(
intArrayStream
.through(fs2.data.json.render.prettyPrint())
.compile
.drain)

@Benchmark
def objectPretty(bh: Blackhole) =
bh.consume(
objectStream
.through(fs2.data.json.render.prettyPrint())
.compile
.drain)

@Benchmark
def intArrayPrettyLegacy(bh: Blackhole) =
bh.consume(
intArrayStream
.through(fs2.data.json.render.pretty())
.compile
.drain)

@Benchmark
def objectPrettyLegacy(bh: Blackhole) =
bh.consume(
objectStream
.through(fs2.data.json.render.pretty())
.compile
.drain)

}
41 changes: 40 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,46 @@ lazy val text = crossProject(JVMPlatform, JSPlatform, NativePlatform)
ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.data.text.CharLikeStringChunks.pullNext"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.data.text.CharLikeStringChunks.advance"),
ProblemFilters.exclude[IncompatibleMethTypeProblem]("fs2.data.text.CharLikeStringChunks.current"),
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.CharLikeStringChunks$StringContext")
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.CharLikeStringChunks$StringContext"),
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$AlignBegin"),
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$AlignBegin$"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignBegin.apply"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignBegin.unapply"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"fs2.data.text.render.internal.Annotated#AlignBegin.fromProduct"),
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$AlignEnd"),
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$AlignEnd$"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignEnd.apply"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#AlignEnd.unapply"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"fs2.data.text.render.internal.Annotated#AlignEnd.fromProduct"),
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$GroupEnd"),
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$GroupEnd$"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#GroupEnd.apply"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#GroupEnd.unapply"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"fs2.data.text.render.internal.Annotated#GroupEnd.fromProduct"),
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$IndentBegin"),
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$IndentBegin$"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentBegin.apply"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentBegin.unapply"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"fs2.data.text.render.internal.Annotated#IndentBegin.fromProduct"),
ProblemFilters.exclude[MissingClassProblem]("fs2.data.text.render.internal.Annotated$IndentEnd"),
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$IndentEnd$"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentEnd.apply"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#IndentEnd.unapply"),
ProblemFilters.exclude[IncompatibleResultTypeProblem](
"fs2.data.text.render.internal.Annotated#IndentEnd.fromProduct"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Line.hp"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#LineBreak.hp"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.hp"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.copy"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.copy$default$2"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.this"),
ProblemFilters.exclude[MissingTypesProblem]("fs2.data.text.render.internal.Annotated$Text$"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text.apply"),
ProblemFilters.exclude[DirectMissingMethodProblem]("fs2.data.text.render.internal.Annotated#Text._2")
)
)
.nativeSettings(
Expand Down
66 changes: 65 additions & 1 deletion json/src/main/scala/fs2/data/json/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,71 @@ package object json {
* You can use this to write the Json stream to a file.
*/
def compact[F[_]]: Pipe[F, Token, String] =
_.through(fs2.data.text.render.pretty(width = Int.MaxValue)(Token.compact))
_.scanChunks((0, false)) { case (state, chunk) =>
val builder = new StringBuilder
val state1 =
chunk.foldLeft(state) {
case ((level, comma), Token.StartObject) =>
if (comma) {
builder.append(',')
}
builder.append(('{'))
(level + 1, false)
case ((level, _), Token.EndObject) =>
builder.append('}')
(level - 1, level > 1)
case ((level, comma), Token.StartArray) =>
if (comma) {
builder.append(',')
}
builder.append(('['))
(level + 1, false)
case ((level, _), Token.EndArray) =>
builder.append(']')
(level - 1, level > 1)
case ((level, comma), Token.Key(key)) =>
if (comma) {
builder.append(',')
}
builder.append('"')
Token.renderString(key, 0, builder)
builder.append("\":")
(level, false)
case ((level, comma), Token.StringValue(key)) =>
if (comma) {
builder.append(',')
}
builder.append('"')
Token.renderString(key, 0, builder)
builder.append('"')
(level, level > 0)
case ((level, comma), Token.NumberValue(n)) =>
if (comma) {
builder.append(',')
}
builder.append(n)
(level, level > 0)
case ((level, comma), Token.TrueValue) =>
if (comma) {
builder.append(',')
}
builder.append("true")
(level, level > 0)
case ((level, comma), Token.FalseValue) =>
if (comma) {
builder.append(',')
}
builder.append("false")
(level, level > 0)
case ((level, comma), Token.NullValue) =>
if (comma) {
builder.append(',')
}
builder.append("null")
(level, level > 0)
}
(state1, Chunk.singleton(builder.result()))
}

/** Renders a pretty-printed representation of the token stream with the given
* indentation size.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ package fs2.data.text.render.internal

private sealed trait Annotated
private object Annotated {
case class Text(text: String, hp: Int) extends Annotated
case class Line(hp: Int) extends Annotated
case class LineBreak(hp: Int) extends Annotated
case class Text(text: String) extends Annotated
case class Line(pos: Int) extends Annotated
case class LineBreak(pos: Int) extends Annotated
case class GroupBegin(hpl: Position) extends Annotated
case class GroupEnd(hp: Int) extends Annotated
case class IndentBegin(hp: Int) extends Annotated
case class IndentEnd(hp: Int) extends Annotated
case class AlignBegin(hp: Int) extends Annotated
case class AlignEnd(hp: Int) extends Annotated
case object GroupEnd extends Annotated
case object IndentBegin extends Annotated
case object IndentEnd extends Annotated
case object AlignBegin extends Annotated
case object AlignEnd extends Annotated
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2024 fs2-data Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fs2.data.text.render.internal

private sealed trait NonEmptyIntList {
def head: Int
def ::(i: Int): NonEmptyIntList =
More(i, this)
def incHead: NonEmptyIntList
def decHead: NonEmptyIntList
def pop: NonEmptyIntList
}
private final case class One(head: Int) extends NonEmptyIntList {
override def incHead: NonEmptyIntList = One(head + 1)
override def decHead: NonEmptyIntList = One(head - 1)
override lazy val pop: NonEmptyIntList = One(0)
}
private final case class More(head: Int, tail: NonEmptyIntList) extends NonEmptyIntList {
override def incHead: NonEmptyIntList = More(head + 1, tail)
override def decHead: NonEmptyIntList = More(head - 1, tail)
override def pop: NonEmptyIntList = tail
}
Loading

0 comments on commit 5baf371

Please sign in to comment.