Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the Why Swift on Server section and add page on concurrency. #496

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/stylesheets/_pages.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
@import "pages/getting-started";
@import "pages/swift-evolution";
@import "pages/packages";
@import "pages/server";
41 changes: 41 additions & 0 deletions assets/stylesheets/pages/_server.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.why-swift-on-server {
list-style: none;
display: grid;
gap: 1rem;
padding: 0;

li {
border: 1px solid var(--color-fill-tertiary);
border-radius: var(--border-radius);
padding: 1rem;
display: flex;
flex-direction: column;

h3 {
line-height: 1.4;
font-size: 1.4rem;
padding-top: 0;
}

p {
flex-grow: 1;
color: var(--color-secondary-label);
margin-bottom: 0;
}
}

.cta-secondary {
margin-top: 1rem;
}
}

.why-swift-on-server {
@media (min-width: 1000px) {
grid-template-columns: repeat(2, 1fr);

li:nth-child(2n-1):nth-last-of-type(1) {
grid-column: span 2;
}
}
}

22 changes: 3 additions & 19 deletions documentation/server/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,9 @@ Most importantly, Swift is designed to make writing and maintaining correct prog

## Why Swift on Server?

In addition to the characteristics of Swift that make it an excellent general-purpose programming language,
it also has unique characteristics that make it specifically suitable for Server applications

### Small footprint
One of the main goals of a modern cloud platform is to maximize resource utilization by efficiently packing services into a single machine.
Cloud services built with Swift have a small memory footprint (measured in MB)--especially when compared to other popular server languages with automatic memory management.
Services built with Swift are also CPU-efficient, given the language’s focus on performance.

### Quick startup time
Swift-based applications start quickly since there are almost no warm up operations.
This makes Swift a great fit for cloud services, which are often re-scheduled onto new VMs or containers to address platform formation changes.
Using Swift also helps streamline continuous delivery pipelines, since you incur less wait time for new versions of the service fleet to come online.
Finally, quick boot times make Swift a perfect fit for serverless applications such as Cloud Functions or Lambda with negligible cold start times.

### Deterministic performance
Swift’s use of ARC (instead of tracing garbage collection) and its lack of JIT gives it an important edge in the cloud services space.
While tracing garbage collection technologies have vastly improved in the last few years, they still compete with the application for resources which triggers non-deterministic performance.
The absence of JIT means no runtime optimization or de-optimization.
It’s challenging to debug non-deterministic performance, and language-induced non-deterministic performance can both confuse and mask application-level performance issues that could otherwise be addressed.
Learn why Swift is well suited to developing server-side applications that are elegant and performant.

<a href="/documentation/server/why" class="cta-secondary">Learn more</a>

## Development guides

Expand Down
178 changes: 178 additions & 0 deletions documentation/server/why/concurrency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
layout: page
title: Why Swift on Server - Concurrency
---

Concurrency is often critical for server-side applications in order to minimize latency, or the time it takes an application to complete some task. Many server-side applications are built up by retrieving and combining responses from multiple different downstream services - such as remote databases or other micro-services - and concurrency can help minimize the time a server is just waiting on receiving these responses by making requests in parallel.

Swift has a suite of features that make writing concurrent applications straight-forward while minimizing the chance of introducing difficult-to-debug race conditions.

## Asynchronous Task Execution
Swift has a built-in mechanism for running asynchronous tasks. These tasks are run on a cooperative thread pool and Swift automatically manages suspending tasks when they need to wait for some future event and resuming them when they continue.

The advantage of this mechanism is that, outside of advanced use cases, the optimization of task execution is taken care of you. There is no need to "rightsize" a custom thread pool or executor to find the sweet spot between high CPU utilization and context switching between threads. No worry about thread explosions if you create too much parallel work. No wasted resources as threads just wait around for network responses to return. No verbose syntax to learn. Swift automatically creates the cooperative thread pool optimized for the size of the current computing resource and schedules tasks on it.

## Async/Await syntax

Swift utilizes a built-in "async/await" style syntax that by default hides the complexity of concurrent programming. Similar to how Swift uses the explicit `try` keyword at function call sites to indicate that a function might throw, the `await` keyword is used to indicate that a function may suspend its processing to asynchronously wait on some future event or completion of another task.

~~~ swift
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you verified that ~~~ produce a code block? I thought it was only three backticks that worked

func someFunction() async -> ClientResult {
return await myClient.get() // this call may take significant time
}

let theClientResult = await someFunction()
~~~

## Structured Concurrency
Building on the ability to schedule asynchronous tasks, Swift also makes it easy to create tasks which are the logical child tasks of the currently running task, creating a tree-structure of tasks. This task-tree retains the creation relationship between tasks which can be used by the Swift runtime and applications to optimize the application.

This is particularly useful for request-based server-side applications - such as http request/response applications - where there are often a set of tasks that ultimately need to come together to create a response.

### Child Task Creation

Swift provides a number of mechanisms that allow for the creation and management of child tasks, the most flexible of which are task groups.

~~~ swift
await withTaskGroup(of: TaskResultType.self) { group in
group.addTask {
...
}

group.addTask {
...
}

for await taskResult in group {
// do something with each result as it is returned,
// potentially adding more tasks to the group or
// breaking out of the loop
}

// cancel any tasks still running
group.cancelAll()
}
~~~

Task Groups are extremely powerful, providing complete control over when tasks are added to the group, when and how their results are processed and managing task cleanup/cancellation if required. Task Groups can be used to create simplified high level abstractions such as `concurrentForEach` and `concurrentMap` or alternatively to create complex custom implementations with precise control over specific concurrent use cases.

### Task Locals

Task Locals provide the ability to associate Swift objects - such as structs and classes - with a Swift task. These objects can be associated with a task and then retrieved somewhere else without explicitly passing the objects between the two. This is often useful when these two actions are performed without knowledge of the other, such as across different libraries.

Conceptually Task Locals are similar to Java's Thread Locals but, due to structured concurrency, solve a number of problems with latter. Task Locals are automatically propagated to child tasks avoiding manual, and often error-prone, propagation of context between threads. Task Locals are also only associated with the task, not a particular thread and therefore follow the task regardless if the task is suspended and then resumed by the system on a different thread.

There are a number of core uses of Task Locals in the Swift server-side ecosystem that enable robust development of applications.

1. [`swift-log`](https://github.com/apple/swift-log)'s Metadata Providers: Swift's unified logging API, swift-log, provides the ability to retrieve log metadata from Task Locals and incorporate them into any logs emitted. In http request/response applications for example, this allows any logs - including those emitted from libraries with no knowledge of how the top-level application works - to include log metadata such as requestIds and other information about the incoming request.
1. [swift-distributed-tracing](https://github.com/apple/swift-distributed-tracing): Swift's unified distributed tracing API allows specific tracing implementations to set tracing context from incoming requests and to then set it on any outgoing requests made in that task or any child task.

For most server-side use cases, the [ServiceContext](https://github.com/apple/swift-service-context) type, modeled after the concepts explained in [W3C Baggage](https://w3c.github.io/baggage/) and built on top of Task Locals, will be a convenient mechanism to transport task-specific information. This type easily allows for custom attributes to be stored and retrieved in a type-safe manner. This type is also the basis for `swift-distributed-tracing`'s trace identifier propagation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modeled

Cries in British English 😆



~~~ swift
struct MyData {
let requestId: String
}

extension ServiceContext {
public var myData: MyData? {
get {
self[MyDataKey.self]
}
set {
self[MyDataKey.self] = newValue
}
}
}

private enum MyDataKey: ServiceContextKey {
typealias Value = MyData

static var nameOverride: String? = "my-data"
}

if let myData = ServiceContext.current.myData {
print("Hello \(myData.requestId)")
}
~~~

### Cancellation

Server-side applications often perform work that is time-bounded. For example in a http request/response application after a certain duration the client has likely given up and potentially retried the request, making any additional processing - even if it does eventually complete - ultimately unused.

Swift provides the ability to flag that a task is cancelled and this is automatically propagated to child-tasks. Tasks that are currently suspended will throw a `CancellationError` which parent tasks can catch and perform appropriate cleanup before completing. Other tasks can handle checking for cancellation manually if required. This mechanism allows for the robust cleanup of entire task-trees once it is known that they are no longer needed.

### Task Priority

Structured concurrency also simplifies managing the priority of different tasks. By default tasks inherit the priority of their parent task and additionally Swift will reduce the likelihood of priority inversion by automatically raising the priority of a task to at least the priority of any task suspended waiting for its completion.

## Thread Safety

Server-side applications are often highly concurrent, often with many hundreds or even thousands of requests executing in parallel within the same application. In these cases it is critical for correctness - and potentially even availability - that these requests can all run in parallel without unexpectedly affecting each other. Failing to make applications thread safe often results in unpredictable and difficult-to-debug race conditions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra often

Suggested change
Server-side applications are often highly concurrent, often with many hundreds or even thousands of requests executing in parallel within the same application. In these cases it is critical for correctness - and potentially even availability - that these requests can all run in parallel without unexpectedly affecting each other. Failing to make applications thread safe often results in unpredictable and difficult-to-debug race conditions.
Server-side applications are often highly concurrent, with many hundreds or even thousands of requests executing in parallel within the same application. In these cases it is critical for correctness - and potentially even availability - that these requests can all run in parallel without unexpectedly affecting each other. Failing to make applications thread safe often results in unpredictable and difficult-to-debug race conditions.


From the beginning Swift has focused on providing safe defaults and thread safety is no exception. Swift encourages the use of value types - such as structs - that, even if mutated only mutate the local copy and do not expose shared mutable state through multiple references. Swift's built-in collection types - arrays, dictionaries and sets - all utilize copy-on-write implementations underneath so again do not expose shared mutable state potentially across threads.

In addition Swift is increasingly making its compiler smart enough to enforce thread safety at compile-time. Objects - or even function blocks - sent across task boundaries must either be implicitly thread safe - such as structs - or guaranteed by the compiler to be so, through the `@Sendable` annotation.

While the performance and correctness of server-side applications often depends on the ability for separate requests to run in parallel without interfering with each other, there are times when you explicitly want communication between different threads of execution. For these situations, Swift provides a number of mechanisms so that this communication can be handled correctly and safely.

### Actors

In many instances a server-side application will want a small amount of shared mutable state across the application. Caches of various kinds, for example, ensure that expensive operations - such as retrieving information across a network connection - are minimized and the results reused when possible.

Actors integrate with the "async/await" syntax to ensure that the execution of any functions isolated to the actor will be serialized, with any parallel function calls suspended until they can be run safely on the actor. This protects the mutable state of the actor from concurrent access without the author having to worry about the underlying details of how this happens.

This ensures that any mutable state the actor has can safely be accessed across task boundaries and allows the actor itself to be passed across task boundaries without any unexpected race conditions, that is actors are considered `Sendable`.

~~~ swift
actor Metrics {
var count = 0

func increment() {
self.count += 1
}
}

let metrics = Metrics()

await withTaskGroup(of: Void.self) { group in
group.addTask {
await metrics.increment()
}

group.addTask {
await metrics.increment()
}

await group.waitForAll()
}
~~~

## Async sequences

Async Sequences provide the ability to pass a sequence of `Sendable` Swift objects between tasks over time. They are a powerful mechanism that can be used to implement a variety of producer-consumer use cases. The Swift standard library along with other core libraries - such as [swift-async-algorithms](https://github.com/apple/swift-async-algorithms) - provide a number of ready-to-use implementations of the base `AsyncSequence` protocol, including those that provide back-pressure to ensure the production of values do not exceed the consumption of values.

~~~ swift
// AsyncStream is an implementation of the AsyncSequence protocol provided
// by the standard library
let (stream, continuation) = AsyncStream.makeStream(of: Int.self)

await withTaskGroup(of: Void.self) { group in
group.addTask {
(0..<100).forEach { index in
continuation.yield(index)
}

continuation.finish()
}

group.addTask {
for await value in stream {
// do something with the value
}
}

await group.waitForAll()
}
~~~
27 changes: 27 additions & 0 deletions documentation/server/why/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
layout: page
title: Why Swift on Server
---

Swift brings together a large range of features that make it extremely well suited to enabling elegant yet highly performant server-side applications.

<ul class="why-swift-on-server">
<li>
<h3>Performance</h3>
<p>
Learn how Swift can enable highly and predictably performant server-side applications that can reduce operational expenses.
</p>

<a href="/documentation/server/why/performance" class="cta-secondary">Learn more</a>
</li>
<li>
<h3>Concurrency</h3>
<p>
Creating highly parallelizable and performant applications is critical to many server-side use cases.
<br><br>
Learn how Swift provides a suite of features that make writing concurrent applications easier and less error prone.
</p>

<a href="/documentation/server/why/concurrency" class="cta-secondary">Learn more</a>
</li>
</ul>
23 changes: 23 additions & 0 deletions documentation/server/why/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
layout: page
title: Why Swift on Server - Performance
---

For the same reasons that make Swift is a fantastic choice for embedded and constrained environments such as watches and phones, Swift makes it possible to write simple, elegant server-side applications that are also highly efficient.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph needs reworking. There's a couple of grammar errors and the flow is a little off


## Small footprint
One of the main goals of a modern cloud platform is to maximize resource utilization by efficiently packing services into a single machine.
Cloud services built with Swift have a small memory footprint (measured in MB)--especially when compared to other popular server languages with automatic memory management.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Cloud services built with Swift have a small memory footprint (measured in MB)--especially when compared to other popular server languages with automatic memory management.
Cloud services built with Swift have a small memory footprint (measured in MB) - especially when compared to other popular server languages with automatic memory management.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth having something on safety here as well? It might end up being a separate section though

Services built with Swift are also CPU-efficient, given the language’s focus on performance.

## Quick startup time
Swift-based applications start quickly since there are almost no warm up operations.
This makes Swift a great fit for cloud services, which are often re-scheduled onto new VMs or containers to address platform formation changes.
Using Swift also helps streamline continuous delivery pipelines, since you incur less wait time for new versions of the service fleet to come online.
Finally, quick boot times make Swift a perfect fit for serverless applications such as Cloud Functions or Lambda with negligible cold start times.

## Deterministic performance
Swift’s use of ARC (instead of tracing garbage collection) and its lack of JIT gives it an important edge in the cloud services space.
While tracing garbage collection technologies have vastly improved in the last few years, they still compete with the application for resources which triggers non-deterministic performance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
While tracing garbage collection technologies have vastly improved in the last few years, they still compete with the application for resources which triggers non-deterministic performance.
While tracing garbage collection technologies have vastly improved in the last few years, they still compete with the application for resources when triggered which results in non-deterministic performance.

The absence of JIT means no runtime optimization or de-optimization.
It’s challenging to debug non-deterministic performance, and language-induced non-deterministic performance can both confuse and mask application-level performance issues that could otherwise be addressed.