Skip to content

Commit 7a3419a

Browse files
committed
PlatformFileOps: read/write files asynchronously
Previously, we simply wrapped a Future around a blocking file I/O both on JVM and Native; while we still keep this approach on Native, let's implement actual asynchronous file I/O on JVM, just like on JS. Also, modify runners to use this async writing and a different thread pool for that, so that it does not clash with formatting tasks.
1 parent 27a5191 commit 7a3419a

File tree

12 files changed

+198
-53
lines changed

12 files changed

+198
-53
lines changed

build.sbt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ inThisBuild {
4040
crossScalaVersions := List(scala213, scala212),
4141
resolvers ++= Resolver.sonatypeOssRepos("releases"),
4242
resolvers ++= Resolver.sonatypeOssRepos("snapshots"),
43-
resolvers +=
44-
"Sonatype Releases"
45-
.at("https://oss.sonatype.org/content/repositories/releases"),
4643
testFrameworks += new TestFramework("munit.Framework"),
4744
// causes native image issues
4845
dependencyOverrides += "org.jline" % "jline" % "3.29.0",

scalafmt-cli/jvm/src/main/scala/org/scalafmt/cli/ScalafmtDynamicRunner.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.scalafmt.dynamic.ScalafmtDynamicError
55
import org.scalafmt.interfaces.Scalafmt
66
import org.scalafmt.interfaces.ScalafmtSession
77
import org.scalafmt.sysops.PlatformFileOps
8+
import org.scalafmt.sysops.PlatformRunOps
89

910
import java.nio.file.Path
1011

@@ -52,10 +53,11 @@ object ScalafmtDynamicRunner extends ScalafmtRunner {
5253
inputMethod: InputMethod,
5354
session: ScalafmtSession,
5455
options: CliOptions,
55-
): Future[ExitCode] = inputMethod.readInput(options).map { input =>
56-
val formatResult = session.format(inputMethod.path, input)
57-
inputMethod.write(formatResult, input, options)
58-
}
56+
): Future[ExitCode] = inputMethod.readInput(options)
57+
.map(code => code -> session.format(inputMethod.path, code))
58+
.flatMap { case (code, formattedCode) =>
59+
inputMethod.write(formattedCode, code, options)
60+
}(PlatformRunOps.ioExecutionContext)
5961

6062
private def getFileMatcher(paths: Seq[Path]): Path => Boolean = {
6163
val dirBuilder = Seq.newBuilder[Path]

scalafmt-cli/shared/src/main/scala/org/scalafmt/cli/CliOptions.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ case class CliOptions(
9696
quiet: Boolean = false,
9797
stdIn: Boolean = false,
9898
noStdErr: Boolean = false,
99-
error: Boolean = false,
99+
private val error: Boolean = false,
100100
check: Boolean = false,
101101
) {
102102
val writeMode: WriteMode = writeModeOpt.getOrElse(WriteMode.Override)
@@ -205,4 +205,7 @@ case class CliOptions(
205205
*/
206206
private[cli] def getVersionOpt: Option[String] = getHoconValueOpt(_.version)
207207

208+
private[cli] def exitCodeOnChange =
209+
if (error) ExitCode.TestError else ExitCode.Ok
210+
208211
}

scalafmt-cli/shared/src/main/scala/org/scalafmt/cli/InputMethod.scala

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.scalafmt.sysops.AbsoluteFile
55
import org.scalafmt.sysops.FileOps
66
import org.scalafmt.sysops.PlatformCompat
77
import org.scalafmt.sysops.PlatformFileOps
8+
import org.scalafmt.sysops.PlatformRunOps
89

910
import java.io.InputStream
1011
import java.nio.file.Path
@@ -20,28 +21,30 @@ sealed abstract class InputMethod {
2021

2122
protected def print(text: String, options: CliOptions): Unit
2223
protected def list(options: CliOptions): Unit
23-
protected def overwrite(text: String, options: CliOptions): Unit
24+
protected def overwrite(text: String, options: CliOptions): Future[Unit]
2425

2526
final def write(
2627
formatted: String,
2728
original: String,
2829
options: CliOptions,
29-
): ExitCode = {
30-
val codeChanged = formatted != original
31-
if (options.writeMode == WriteMode.Stdout) print(formatted, options)
32-
else if (codeChanged) options.writeMode match {
30+
): Future[ExitCode] = {
31+
def codeChanged = formatted != original
32+
def exitCode = if (codeChanged) options.exitCodeOnChange else ExitCode.Ok
33+
options.writeMode match {
34+
case WriteMode.Stdout => print(formatted, options); exitCode.future
35+
case _ if !codeChanged => ExitCode.Ok.future
36+
case WriteMode.List => list(options); options.exitCodeOnChange.future
37+
case WriteMode.Override => overwrite(formatted, options)
38+
.map(_ => options.exitCodeOnChange)(PlatformRunOps.ioExecutionContext)
3339
case WriteMode.Test =>
3440
val pathStr = path.toString
3541
val diff = InputMethod.unifiedDiff(pathStr, original, formatted)
3642
val msg =
3743
if (diff.nonEmpty) diff
3844
else s"--- +$pathStr\n => modified line endings only"
39-
throw MisformattedFile(path, msg)
40-
case WriteMode.Override => overwrite(formatted, options)
41-
case WriteMode.List => list(options)
42-
case _ =>
45+
Future.failed(MisformattedFile(path, msg))
46+
case _ => options.exitCodeOnChange.future
4347
}
44-
if (options.error && codeChanged) ExitCode.TestError else ExitCode.Ok
4548
}
4649

4750
}
@@ -61,8 +64,10 @@ object InputMethod {
6164
override protected def print(text: String, options: CliOptions): Unit =
6265
options.common.out.print(text)
6366

64-
override protected def overwrite(text: String, options: CliOptions): Unit =
65-
print(text, options)
67+
override protected def overwrite(
68+
text: String,
69+
options: CliOptions,
70+
): Future[Unit] = Future.successful(print(text, options))
6671

6772
override protected def list(options: CliOptions): Unit = options.common.out
6873
.println(filename)
@@ -76,8 +81,11 @@ object InputMethod {
7681
override protected def print(text: String, options: CliOptions): Unit =
7782
options.common.out.print(text)
7883

79-
override protected def overwrite(text: String, options: CliOptions): Unit =
80-
file.writeFile(text)(options.encoding)
84+
override protected def overwrite(
85+
text: String,
86+
options: CliOptions,
87+
): Future[Unit] = PlatformFileOps
88+
.writeFileAsync(file.path, text)(options.encoding)
8189

8290
override protected def list(options: CliOptions): Unit = options.common.out
8391
.println(PlatformCompat.relativize(options.cwd, file))

scalafmt-cli/shared/src/main/scala/org/scalafmt/cli/ScalafmtCoreRunner.scala

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.scalafmt.Versions
77
import org.scalafmt.config.ProjectFiles
88
import org.scalafmt.config.ScalafmtConfig
99
import org.scalafmt.config.ScalafmtConfigException
10+
import org.scalafmt.sysops.PlatformRunOps
1011

1112
import scala.meta.parsers.ParseException
1213
import scala.meta.tokenizers.TokenizeException
@@ -62,24 +63,32 @@ object ScalafmtCoreRunner extends ScalafmtRunner {
6263
inputMethod: InputMethod,
6364
options: CliOptions,
6465
scalafmtConfig: ScalafmtConfig,
65-
): Future[ExitCode] = inputMethod.readInput(options).map { input =>
66-
val filename = inputMethod.path.toString
66+
): Future[ExitCode] = {
67+
val path = inputMethod.path.toString
6768
@tailrec
6869
def handleError(e: Throwable): ExitCode = e match {
6970
case Error.WithCode(e, _) => handleError(e)
7071
case _: ParseException | _: TokenizeException =>
7172
options.common.err.println(e.toString)
7273
ExitCode.ParseError
7374
case e =>
74-
new FailedToFormat(filename, e).printStackTrace(options.common.err)
75+
new FailedToFormat(path, e).printStackTrace(options.common.err)
7576
ExitCode.UnexpectedError
7677
}
77-
Scalafmt.formatCode(input, scalafmtConfig, options.range, filename)
78-
.formatted match {
79-
case Formatted.Success(x) => inputMethod.write(x, input, options)
80-
case x: Formatted.Failure =>
81-
if (scalafmtConfig.runner.ignoreWarnings) ExitCode.Ok // do nothing
82-
else handleError(x.e)
83-
}
78+
inputMethod.readInput(options).map { code =>
79+
val res = Scalafmt.formatCode(code, scalafmtConfig, options.range, path)
80+
res.formatted match {
81+
case x: Formatted.Success => Right(code -> x.formattedCode)
82+
case x: Formatted.Failure => Left(
83+
if (res.config.runner.ignoreWarnings) ExitCode.Ok // do nothing
84+
else handleError(x.e),
85+
)
86+
}
87+
}.flatMap {
88+
case Right((code, formattedCode)) => inputMethod
89+
.write(formattedCode, code, options)
90+
case Left(exitCode) => exitCode.future
91+
}(PlatformRunOps.ioExecutionContext)
8492
}
93+
8594
}

scalafmt-sysops/js/src/main/scala/org/scalafmt/sysops/PlatformFileOps.scala

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package org.scalafmt.sysops
22

3-
//import org.scalafmt.CompatCollections.JavaConverters._
4-
53
import scala.meta.internal.io._
64

75
import java.nio.file._
86

97
import scala.concurrent.Future
10-
import scala.concurrent.Promise
118
import scala.io.Codec
129
import scala.scalajs.js
1310
import scala.util.Try
@@ -77,19 +74,18 @@ object PlatformFileOps {
7774
def readFile(path: Path)(implicit codec: Codec): String = JSFs
7875
.readFileSync(path.toString, codec.name)
7976

80-
def readFileAsync(file: Path)(implicit codec: Codec): Future[String] = {
81-
val promise = Promise[String]()
82-
def cb(err: js.Error, res: String): Unit =
83-
if (err == null) promise.trySuccess(res)
84-
else promise.tryFailure(new RuntimeException(err.message))
85-
JSFs.readFile(file.toString, codec.name, cb)
86-
promise.future
87-
}
77+
def readFileAsync(file: Path)(implicit codec: Codec): Future[String] =
78+
JSFsPromises.readFile(file.toString, codec.name).toFuture
8879

8980
def readStdinAsync: Future[String] = JSIO.readStdinAsync
9081

9182
def writeFile(path: Path, content: String)(implicit codec: Codec): Unit = JSFs
9283
.writeFileSync(path.toString, content, codec.name)
9384

85+
def writeFileAsync(file: Path, data: String)(implicit
86+
codec: Codec,
87+
): Future[Unit] = JSFsPromises.writeFile(file.toString, data, codec.name)
88+
.toFuture
89+
9490
def cwd() = js.Dynamic.global.process.cwd().asInstanceOf[String]
9591
}

scalafmt-sysops/js/src/main/scala/org/scalafmt/sysops/PlatformRunOps.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ private[scalafmt] object PlatformRunOps {
1515
implicit def executionContext: ExecutionContext =
1616
scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
1717

18+
def ioExecutionContext: ExecutionContext = executionContext
19+
1820
def getSingleThreadExecutionContext: ExecutionContext = executionContext // same one
1921

2022
def runArgv(cmd: Seq[String], cwd: Option[Path]): Try[String] = {

scalafmt-sysops/jvm-native/src/main/scala/org/scalafmt/sysops/PlatformFileOps.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ object PlatformFileOps {
5454
def readFile(file: Path)(implicit codec: Codec): String =
5555
new String(Files.readAllBytes(file), codec.charSet)
5656

57-
def readFileAsync(file: Path)(implicit codec: Codec): Future[String] = Future
58-
.successful(readFile(file))
57+
def readFileAsync(file: Path)(implicit codec: Codec): Future[String] =
58+
GranularPlatformAsyncOps.readFileAsync(file)
5959

6060
def readStdinAsync: Future[String] = Future
6161
.successful(FileOps.readInputStream(System.in))
@@ -65,5 +65,9 @@ object PlatformFileOps {
6565
Files.write(path, bytes)
6666
}
6767

68+
def writeFileAsync(path: Path, content: String)(implicit
69+
codec: Codec,
70+
): Future[Unit] = GranularPlatformAsyncOps.writeFileAsync(path, content)
71+
6872
def cwd() = System.getProperty("user.dir")
6973
}

scalafmt-sysops/jvm-native/src/main/scala/org/scalafmt/sysops/PlatformRunOps.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ private[scalafmt] object PlatformRunOps {
1313

1414
implicit def executionContext: ExecutionContext = ExecutionContext.global
1515

16+
def ioExecutionContext: ExecutionContext =
17+
GranularPlatformAsyncOps.ioExecutionContext
18+
1619
def getSingleThreadExecutionContext: ExecutionContext = ExecutionContext
1720
.fromExecutor(Executors.newSingleThreadExecutor())
1821

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package org.scalafmt.sysops
2+
3+
import java.io.ByteArrayOutputStream
4+
import java.nio.ByteBuffer
5+
import java.nio.channels.AsynchronousFileChannel
6+
import java.nio.channels.CompletionHandler
7+
import java.nio.file.Path
8+
import java.nio.file.StandardOpenOption
9+
import java.util.concurrent.Executors
10+
11+
import scala.concurrent.ExecutionContext
12+
import scala.concurrent.Future
13+
import scala.concurrent.Promise
14+
import scala.io.Codec
15+
import scala.util.Try
16+
17+
private[sysops] object GranularPlatformAsyncOps {
18+
19+
implicit val ioExecutionContext: ExecutionContext = ExecutionContext
20+
.fromExecutor(Executors.newCachedThreadPool())
21+
22+
def readFileAsync(path: Path)(implicit codec: Codec): Future[String] = {
23+
val promise = Promise[String]()
24+
25+
val buf = new Array[Byte](1024)
26+
val bbuf = ByteBuffer.wrap(buf)
27+
val os = new ByteArrayOutputStream()
28+
29+
Try {
30+
val channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)
31+
val handler = new CompletionHandler[Integer, AnyRef] {
32+
override def completed(result: Integer, unused: AnyRef): Unit = {
33+
val count = result.intValue()
34+
if (count < 0) {
35+
promise.trySuccess(os.toString(codec.charSet.name()))
36+
channel.close()
37+
} else {
38+
if (count > 0) {
39+
os.write(buf, 0, count)
40+
bbuf.clear()
41+
}
42+
channel.read(bbuf, os.size(), null, this)
43+
}
44+
}
45+
override def failed(exc: Throwable, unused: AnyRef): Unit = {
46+
promise.tryFailure(exc)
47+
channel.close()
48+
}
49+
}
50+
51+
channel.read(bbuf, 0, null, handler)
52+
}.failed.foreach(promise.tryFailure)
53+
54+
promise.future
55+
}
56+
57+
def writeFileAsync(path: Path, content: String)(implicit
58+
codec: Codec,
59+
): Future[Unit] = {
60+
val promise = Promise[Unit]()
61+
val buf = ByteBuffer.wrap(content.getBytes(codec.charSet))
62+
63+
Try {
64+
val channel = AsynchronousFileChannel.open(
65+
path,
66+
StandardOpenOption.CREATE,
67+
StandardOpenOption.WRITE,
68+
StandardOpenOption.TRUNCATE_EXISTING,
69+
)
70+
71+
val handler = new CompletionHandler[Integer, AnyRef] {
72+
override def completed(result: Integer, attachment: AnyRef): Unit =
73+
if (buf.hasRemaining) channel.write(buf, buf.position(), null, this)
74+
else {
75+
promise.trySuccess(())
76+
channel.close()
77+
}
78+
79+
override def failed(exc: Throwable, attachment: AnyRef): Unit = {
80+
promise.tryFailure(exc)
81+
channel.close()
82+
}
83+
}
84+
85+
channel.write(buf, 0L, null, handler)
86+
}.failed.foreach(promise.tryFailure)
87+
88+
promise.future
89+
}
90+
91+
}

0 commit comments

Comments
 (0)