mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1830 from sixten/warnings
Address iOS compiler and linter warnings
This commit is contained in:
commit
8068b11f1e
173 changed files with 57894 additions and 367 deletions
49
.github/workflows/run-distiller.yml
vendored
Normal file
49
.github/workflows/run-distiller.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
name: Run DomDistiller on test pages
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/readabilityjs/test/test-pages/**'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run-distiller:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Generate distiller output for readability
|
||||
run: |
|
||||
go install github.com/omnivore-app/go-domdistiller@latest
|
||||
for f in packages/readabilityjs/test/test-pages/*; do
|
||||
echo "Processing $f file..."
|
||||
# skip newsletters for now
|
||||
if [ "$f" = "packages/readabilityjs/test/test-pages/newsletters" ]; then
|
||||
continue
|
||||
fi
|
||||
go-domdistiller file -i $f/source.html -o $f/distiller.html
|
||||
done
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Generate static html
|
||||
run: |
|
||||
cd packages/readabilityjs/test
|
||||
python generate_static.py
|
||||
- name: Commit changes and push
|
||||
uses: EndBug/add-and-commit@v9
|
||||
with:
|
||||
add: '.'
|
||||
message: 'Update generated html'
|
||||
push: true
|
||||
38
.github/workflows/static.yml
vendored
Normal file
38
.github/workflows/static.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
name: Deploy static content to Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/readabilityjs/test/index.html'
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: 'packages/readabilityjs/test'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
|
|
@ -64,8 +64,8 @@ final class AppStoreScreenshots: XCTestCase {
|
|||
//
|
||||
snapshot("ReaderTTSPlayer")
|
||||
|
||||
/// app.children(matching: .window).element(boundBy: 0).tap()
|
||||
// XCUIApplication().buttons["Back"].tap()
|
||||
// app.children(matching: .window).element(boundBy: 0).tap()
|
||||
// XCUIApplication().buttons["Back"].tap()
|
||||
}
|
||||
|
||||
func testScreenshotReaderActions() throws {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// swiftlint:disable line_length
|
||||
//
|
||||
// SnapshotHelper.swift
|
||||
// Example
|
||||
|
|
@ -32,7 +33,8 @@ func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
|
|||
|
||||
/// - Parameters:
|
||||
/// - name: The name of the snapshot
|
||||
/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
|
||||
/// - timeout: Amount of seconds to wait until the network loading indicator disappears.
|
||||
/// Pass `0` if you don't want to wait.
|
||||
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
|
||||
Snapshot.snapshot(name, timeWaitingForIdle: timeout)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
045B1681279147E7005047F7 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 045B1680279147E7005047F7 /* FirebaseMessaging */; };
|
||||
0465B9BE26CDD35F005558CD /* MainApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81BE98F0CB588F5FC577A13 /* MainApp.swift */; };
|
||||
046C5CD526A3F89A00AC5349 /* icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 046C5CD426A3F89A00AC5349 /* icon.icns */; };
|
||||
046C5CDF26A3F89A00AC5349 /* ShareExtension-Mac.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 046C5CD226A3F89A00AC5349 /* ShareExtension-Mac.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
046C5CDF26A3F89A00AC5349 /* ShareExtension-Mac.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 046C5CD226A3F89A00AC5349 /* ShareExtension-Mac.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
046C5CE626A3FE8C00AC5349 /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 046C5CE526A3FE8C00AC5349 /* App */; };
|
||||
047AD6F22724C934004FD5CF /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 047AD6F12724C934004FD5CF /* App */; };
|
||||
047AD6F42724C941004FD5CF /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 047AD6F32724C941004FD5CF /* App */; };
|
||||
|
|
@ -30,10 +30,10 @@
|
|||
04E9707226BDE5F1002A523D /* ShareExtension.js in Resources */ = {isa = PBXBuildFile; fileRef = 04E9706E26BDE47D002A523D /* ShareExtension.js */; };
|
||||
04EE742126DFD2EF001B3483 /* ScreenIdentifierHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04EE741F26DFD2CA001B3483 /* ScreenIdentifierHelpers.swift */; };
|
||||
04EE742226DFD2F0001B3483 /* ScreenIdentifierHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04EE741F26DFD2CA001B3483 /* ScreenIdentifierHelpers.swift */; };
|
||||
1BE07906EAA5555B6140B6DB /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 95D337DDCEA2007A9705A969 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
1BE07906EAA5555B6140B6DB /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 95D337DDCEA2007A9705A969 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
260BBE4168E3C57D6D2E225D /* App in Frameworks */ = {isa = PBXBuildFile; productRef = 7DD0EF4D497D71D248C7B0DF /* App */; };
|
||||
4214F6EF2714D2260096B644 /* SafariExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 42FF1B16271154A700B38C38 /* SafariExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
4214F6F22714D28E0096B644 /* SafariExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 42FF1B20271154A700B38C38 /* SafariExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
4214F6EF2714D2260096B644 /* SafariExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 42FF1B16271154A700B38C38 /* SafariExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
4214F6F22714D28E0096B644 /* SafariExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 42FF1B20271154A700B38C38 /* SafariExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
4225D59227164F82002A680F /* fonts in Resources */ = {isa = PBXBuildFile; fileRef = 4225D59127164F82002A680F /* fonts */; };
|
||||
4225D59327164F82002A680F /* fonts in Resources */ = {isa = PBXBuildFile; fileRef = 4225D59127164F82002A680F /* fonts */; };
|
||||
42321E852714E6B00056429F /* scripts in Resources */ = {isa = PBXBuildFile; fileRef = 42321E822714E6B00056429F /* scripts */; };
|
||||
|
|
@ -127,16 +127,16 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
046C5CE026A3F89A00AC5349 /* Embed App Extensions */ = {
|
||||
046C5CE026A3F89A00AC5349 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
046C5CDF26A3F89A00AC5349 /* ShareExtension-Mac.appex in Embed App Extensions */,
|
||||
4214F6F22714D28E0096B644 /* SafariExtension.appex in Embed App Extensions */,
|
||||
046C5CDF26A3F89A00AC5349 /* ShareExtension-Mac.appex in Embed Foundation Extensions */,
|
||||
4214F6F22714D28E0096B644 /* SafariExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
089E832985F9DC4E3D727F87 /* Embed Frameworks */ = {
|
||||
|
|
@ -169,15 +169,15 @@
|
|||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BA17F8C8424E5121F1EE345C /* Embed App Extensions */ = {
|
||||
BA17F8C8424E5121F1EE345C /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
1BE07906EAA5555B6140B6DB /* ShareExtension.appex in Embed App Extensions */,
|
||||
4214F6EF2714D2260096B644 /* SafariExtension.appex in Embed App Extensions */,
|
||||
1BE07906EAA5555B6140B6DB /* ShareExtension.appex in Embed Foundation Extensions */,
|
||||
4214F6EF2714D2260096B644 /* SafariExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
|
@ -640,8 +640,8 @@
|
|||
048ECFF826A0B1CB00469E57 /* Sources */,
|
||||
048ECFF926A0B1CB00469E57 /* Frameworks */,
|
||||
048ECFFA26A0B1CB00469E57 /* Resources */,
|
||||
04B8B00426A0C19900D3ED0C /* ShellScript */,
|
||||
046C5CE026A3F89A00AC5349 /* Embed App Extensions */,
|
||||
04B8B00426A0C19900D3ED0C /* Format and Lint */,
|
||||
046C5CE026A3F89A00AC5349 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
|
@ -663,7 +663,6 @@
|
|||
buildPhases = (
|
||||
43512AEB8FEA59E4C1B62535 /* Sources */,
|
||||
B147F11D6548C5B600AC0CBD /* Resources */,
|
||||
AAC826E9061AEBA0BF527814 /* Embed Precompiled Frameworks */,
|
||||
76DAC57C99AFCDF06896A42F /* Embed Frameworks */,
|
||||
3C5F49DFFF5F79D9D0C7412E /* Frameworks */,
|
||||
);
|
||||
|
|
@ -743,8 +742,7 @@
|
|||
buildPhases = (
|
||||
8E4C1300A69A749FE3B720A3 /* Sources */,
|
||||
ABA2CCAE947C7AFC88C991A5 /* Resources */,
|
||||
BA17F8C8424E5121F1EE345C /* Embed App Extensions */,
|
||||
6FD84AF08CA126EA8F6906A4 /* Embed Precompiled Frameworks */,
|
||||
BA17F8C8424E5121F1EE345C /* Embed Foundation Extensions */,
|
||||
4C02552307C731B3D3F28680 /* Embed Frameworks */,
|
||||
F06D5ADCFDD68AC8775386BB /* Frameworks */,
|
||||
909EF91C2946F26E9123C65F /* Format and Lint */,
|
||||
|
|
@ -772,7 +770,6 @@
|
|||
buildPhases = (
|
||||
041E0C13E0057CDDC868A4DB /* Sources */,
|
||||
BC2DC89A3D7E1E86540ECA14 /* Resources */,
|
||||
DEA43E9E9C46BFC9EBE0D359 /* Embed Precompiled Frameworks */,
|
||||
089E832985F9DC4E3D727F87 /* Embed Frameworks */,
|
||||
7D03721CA2BD53145181C839 /* Frameworks */,
|
||||
);
|
||||
|
|
@ -793,7 +790,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1400;
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1420;
|
||||
TargetAttributes = {
|
||||
0411792626A22860004AE24F = {
|
||||
CreatedOnToolsVersion = 12.5;
|
||||
|
|
@ -962,39 +959,9 @@
|
|||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
04B8B00426A0C19900D3ED0C /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"$SRCROOT\"/Scripts/format_and_lint.sh \n";
|
||||
};
|
||||
6FD84AF08CA126EA8F6906A4 /* Embed Precompiled Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Embed Precompiled Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "echo \"Skipping, nothing to be embedded.\"";
|
||||
};
|
||||
909EF91C2946F26E9123C65F /* Format and Lint */ = {
|
||||
04B8B00426A0C19900D3ED0C /* Format and Lint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
|
|
@ -1011,33 +978,24 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "\"$SRCROOT\"/Scripts/format_and_lint.sh \n";
|
||||
};
|
||||
AAC826E9061AEBA0BF527814 /* Embed Precompiled Frameworks */ = {
|
||||
909EF91C2946F26E9123C65F /* Format and Lint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Embed Precompiled Frameworks";
|
||||
name = "Format and Lint";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "echo \"Skipping, nothing to be embedded.\"";
|
||||
};
|
||||
DEA43E9E9C46BFC9EBE0D359 /* Embed Precompiled Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Embed Precompiled Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "echo \"Skipping, nothing to be embedded.\"";
|
||||
shellScript = "\"$SRCROOT\"/Scripts/format_and_lint.sh \n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
|
|
@ -1208,6 +1166,7 @@
|
|||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
INFOPLIST_FILE = InfoPlists/MacUnitTests.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
|
@ -1234,6 +1193,7 @@
|
|||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
INFOPLIST_FILE = InfoPlists/MacUnitTests.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
|
@ -1311,6 +1271,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist;
|
||||
|
|
@ -1343,6 +1304,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist;
|
||||
|
|
@ -1370,6 +1332,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
INFOPLIST_FILE = InfoPlists/MacUITests.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
|
@ -1395,6 +1358,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
INFOPLIST_FILE = InfoPlists/MacUITests.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
|
@ -1425,6 +1389,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
|
|
@ -1459,6 +1424,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
|
|
@ -1669,6 +1635,7 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -1708,6 +1675,7 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 80;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = QJF2XZ86HB;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -1773,6 +1741,7 @@
|
|||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
|
|
@ -1940,6 +1909,7 @@
|
|||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1400"
|
||||
LastUpgradeVersion = "1420"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1420"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1420"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
{
|
||||
"identity" : "analytics-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "git@github.com:segmentio/analytics-swift.git",
|
||||
"location" : "https://github.com/segmentio/analytics-swift.git",
|
||||
"state" : {
|
||||
"revision" : "92cc824211160ab98c28c7d40c1e6d27645c2bf1",
|
||||
"version" : "1.2.3"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ var dependencies: [Package.Dependency] {
|
|||
.package(url: "https://github.com/Square/Valet", from: "4.1.2"),
|
||||
.package(url: "https://github.com/maticzav/swift-graphql", from: "2.3.1"),
|
||||
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"),
|
||||
.package(url: "git@github.com:segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")),
|
||||
.package(url: "https://github.com/segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")),
|
||||
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2")
|
||||
]
|
||||
// Comment out following line for macOS build
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ struct SaveLinkToOmnivoreIntent: AppIntent {
|
|||
do {
|
||||
let services = Services()
|
||||
let requestId = UUID().uuidString.lowercased()
|
||||
let saveResult = try await services.dataService.saveURL(id: requestId, url: link.absoluteString)
|
||||
_ = try await services.dataService.saveURL(id: requestId, url: link.absoluteString)
|
||||
|
||||
return .result(dialog: "Link saved to Omnivore")
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -164,9 +164,10 @@ public class ShareExtensionViewModel: ObservableObject {
|
|||
updateStatusOnMain(requestId: newRequestID, newStatus: .synced)
|
||||
|
||||
// Prefetch the newly saved content
|
||||
if let itemID = newRequestID,
|
||||
let currentViewer = services.dataService.currentViewer?.username,
|
||||
(try? await services.dataService.loadArticleContentWithRetries(itemID: itemID, username: currentViewer)) != nil
|
||||
if
|
||||
let itemID = newRequestID,
|
||||
let currentViewer = services.dataService.currentViewer?.username,
|
||||
(try? await services.dataService.loadArticleContentWithRetries(itemID: itemID, username: currentViewer)) != nil
|
||||
{
|
||||
updateStatusOnMain(requestId: requestId, newStatus: .synced, objectID: linkedItemObjectID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import SwiftUI
|
|||
import Utils
|
||||
import Views
|
||||
|
||||
// swiftlint:disable:next type_body_length
|
||||
// swiftlint:disable file_length type_body_length
|
||||
public struct ShareExtensionView: View {
|
||||
let extensionContext: NSExtensionContext?
|
||||
@StateObject var labelsViewModel = LabelsViewModel()
|
||||
|
|
@ -177,6 +177,7 @@ public struct ShareExtensionView: View {
|
|||
VStack(spacing: 15) {
|
||||
SearchBar(searchTerm: $labelsViewModel.labelSearchFilter)
|
||||
|
||||
// swiftlint:disable line_length
|
||||
ScrollView {
|
||||
LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter),
|
||||
selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter),
|
||||
|
|
@ -199,6 +200,7 @@ public struct ShareExtensionView: View {
|
|||
.buttonStyle(PlainButtonStyle())
|
||||
.padding(10)
|
||||
}.background(Color.appButtonBackground)
|
||||
// swiftlint:enable line_length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import Combine
|
|||
import SwiftUI
|
||||
import Utils
|
||||
|
||||
// swiftlint:disable file_length
|
||||
// swiftlint:disable file_length type_body_length
|
||||
#if os(iOS)
|
||||
import PSPDFKit
|
||||
import PSPDFKitUI
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ final class PDFViewerViewModel: ObservableObject {
|
|||
onComplete(pdfItem.highlights.map { $0.patch ?? "" })
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
func createHighlight(
|
||||
dataService: DataService,
|
||||
shortId: String,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
#if os(iOS)
|
||||
|
||||
import Foundation
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Views
|
||||
|
||||
// swiftlint:disable file_length type_body_length
|
||||
public struct ExpandedPlayer: View {
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||
|
|
@ -213,11 +213,11 @@
|
|||
}
|
||||
})
|
||||
// .simultaneousGesture(
|
||||
//// DragGesture().onChanged {
|
||||
//// let isScrollDown = $0.translation.height > 0
|
||||
//// print(isScrollDown)
|
||||
//// }
|
||||
//// )
|
||||
// DragGesture().onChanged {
|
||||
// let isScrollDown = $0.translation.height > 0
|
||||
// print(isScrollDown)
|
||||
// }
|
||||
// )
|
||||
}
|
||||
.background(Color.themeDisabledBG)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ struct HighlightsListCard: View {
|
|||
HStack {
|
||||
Divider()
|
||||
.frame(width: 2)
|
||||
// swiftlint:disable:next line_length
|
||||
.overlay(highlightParams.createdBy != nil ? Color(red: 206 / 255.0, green: 239 / 255.0, blue: 159 / 255.0) : Color.appYellow48)
|
||||
.opacity(0.8)
|
||||
.padding(.top, 2)
|
||||
|
|
|
|||
|
|
@ -45,11 +45,10 @@ struct FeedCardNavigationLink: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Button(
|
||||
action: {
|
||||
viewModel.selectedItem = item
|
||||
viewModel.linkIsActive = true
|
||||
}) {
|
||||
Button {
|
||||
viewModel.selectedItem = item
|
||||
viewModel.linkIsActive = true
|
||||
} label: {
|
||||
NavigationLink(destination: EmptyView()) {
|
||||
EmptyView()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -514,6 +514,7 @@ import Views
|
|||
}
|
||||
}
|
||||
}
|
||||
// swiftlint:disable:next line_length
|
||||
.alert("Are you sure you want to delete this item? All associated notes and highlights will be deleted.", isPresented: $confirmationShown) {
|
||||
Button("Delete Item", role: .destructive) {
|
||||
if let itemToRemove = itemToRemove {
|
||||
|
|
|
|||
|
|
@ -128,9 +128,10 @@ import Views
|
|||
}
|
||||
|
||||
// If possible start prefetching new pages in the background
|
||||
if let itemIDs = syncResult?.updatedItemIDs,
|
||||
let username = dataService.currentViewer?.username,
|
||||
itemIDs.count > 0
|
||||
if
|
||||
let itemIDs = syncResult?.updatedItemIDs,
|
||||
let username = dataService.currentViewer?.username,
|
||||
!itemIDs.isEmpty
|
||||
{
|
||||
Task.detached(priority: .background) {
|
||||
await dataService.prefetchPages(itemIDs: itemIDs, username: username)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import Introspect
|
||||
import Models
|
||||
import Services
|
||||
|
|
|
|||
|
|
@ -44,7 +44,12 @@ import Views
|
|||
let fetchRequest: NSFetchRequest<Models.RecentSearchItem> = RecentSearchItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "term == %@", searchTerm)
|
||||
|
||||
let item = ((try? dataService.viewContext.fetch(fetchRequest))?.first) ?? RecentSearchItem(context: dataService.viewContext)
|
||||
let item: RecentSearchItem
|
||||
if let fetchedItem = (try? dataService.viewContext.fetch(fetchRequest))?.first {
|
||||
item = fetchedItem
|
||||
} else {
|
||||
item = RecentSearchItem(context: dataService.viewContext)
|
||||
}
|
||||
item.term = searchTerm
|
||||
item.savedAt = Date()
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@ struct LabelsMasonaryView: View {
|
|||
@State private var totalHeight = CGFloat.zero
|
||||
private var labelItems: [(label: LinkedItemLabel, selected: Bool)]
|
||||
|
||||
init(labels allLabels: [LinkedItemLabel],
|
||||
selectedLabels: [LinkedItemLabel],
|
||||
onLabelTap: @escaping (LinkedItemLabel, TextChip) -> Void)
|
||||
{
|
||||
init(
|
||||
labels allLabels: [LinkedItemLabel],
|
||||
selectedLabels: [LinkedItemLabel],
|
||||
onLabelTap: @escaping (LinkedItemLabel, TextChip) -> Void
|
||||
) {
|
||||
self.onLabelTap = onLabelTap
|
||||
|
||||
let selected = selectedLabels.map { (label: $0, selected: true) }
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
}
|
||||
.frame(height: storedSize.height)
|
||||
.overlay(content: {
|
||||
// swiftlint:disable line_length
|
||||
HStack {
|
||||
let color: Color = .systemBackground
|
||||
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
|
||||
LinearGradient(colors: [color, color.opacity(0.7), color.opacity(0.5), color.opacity(0.3)].reversed(), startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: 8)
|
||||
// swiftlint:enable line_length
|
||||
}
|
||||
})
|
||||
.disabled(true)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
|
|
@ -20,7 +19,7 @@ import Views
|
|||
func removeToken(dataService: DataService, tokenID: String) {
|
||||
if let idx = devices.firstIndex(where: { $0.id == tokenID }) {
|
||||
Task {
|
||||
try await dataService.syncDeviceToken(deviceTokenOperation: .deleteToken(tokenID: tokenID))
|
||||
_ = try await dataService.syncDeviceToken(deviceTokenOperation: .deleteToken(tokenID: tokenID))
|
||||
devices.remove(at: idx)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
|
|
@ -28,7 +27,7 @@ import Views
|
|||
self.desiredNotificationsEnabled = granted
|
||||
Task {
|
||||
if let savedToken = UserDefaults.standard.string(forKey: UserDefaultKey.firebasePushToken.rawValue) {
|
||||
try? await dataService.syncDeviceToken(
|
||||
_ = try? await dataService.syncDeviceToken(
|
||||
deviceTokenOperation: DeviceTokenOperation.addToken(token: savedToken))
|
||||
}
|
||||
NotificationCenter.default.post(name: Notification.Name("ReconfigurePushNotifications"), object: nil)
|
||||
|
|
|
|||
|
|
@ -154,9 +154,9 @@ struct GroupsView: View {
|
|||
if viewModel.recommendationGroups.count > 0 {
|
||||
Section(header: Text(LocalText.clubsYours)) {
|
||||
ForEach(viewModel.recommendationGroups) { recommendationGroup in
|
||||
let vm = RecommendationsGroupViewModel(recommendationGroup: recommendationGroup)
|
||||
let viewModel = RecommendationsGroupViewModel(recommendationGroup: recommendationGroup)
|
||||
NavigationLink(
|
||||
destination: RecommendationGroupView(viewModel: vm)
|
||||
destination: RecommendationGroupView(viewModel: viewModel)
|
||||
) {
|
||||
Text(recommendationGroup.name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@
|
|||
|
||||
private var innerBody: some View {
|
||||
ForEach(Voices.Languages, id: \.key.self) { language in
|
||||
Button(action: {
|
||||
Button {
|
||||
audioController.defaultLanguage = language.key
|
||||
}) {
|
||||
} label: {
|
||||
HStack {
|
||||
Text(language.name)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import SwiftUI
|
||||
import Views
|
||||
|
||||
// swiftlint:disable line_length
|
||||
struct TextToSpeechView: View {
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
|
||||
|
|
|
|||
|
|
@ -69,8 +69,10 @@
|
|||
}
|
||||
.navigationTitle("Choose a Voice")
|
||||
.onAppear {
|
||||
// swiftlint:disable:next line_length
|
||||
viewModel.realisticVoicesToggle = (audioController.useUltraRealisticVoices && !audioController.ultraRealisticFeatureKey.isEmpty)
|
||||
}.onChange(of: viewModel.realisticVoicesToggle) { value in
|
||||
}
|
||||
.onChange(of: viewModel.realisticVoicesToggle) { value in
|
||||
if value, audioController.ultraRealisticFeatureKey.isEmpty {
|
||||
// User wants to sign up
|
||||
viewModel.waitingForRealisticVoices = true
|
||||
|
|
@ -101,6 +103,7 @@
|
|||
private var ultraRealisticVoices: some View {
|
||||
ForEach([VoiceCategory.enUS], id: \.self) { category in
|
||||
Section(category.rawValue) {
|
||||
// swiftlint:disable:next line_length
|
||||
ForEach(audioController.realisticVoiceList?.filter { $0.category == category } ?? [], id: \.key.self) { voice in
|
||||
voiceRow(for: voice)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ struct SelfHostSettingsView: View {
|
|||
Alert(
|
||||
title: Text("Changing your environment settings will close the app."),
|
||||
dismissButton: .cancel(Text(LocalText.genericOk)) {
|
||||
AppEnvironment.setCustom(serverBaseURL: apiServerAddress, webAppBaseURL: webServerAddress, ttsBaseURL: ttsServerAddress)
|
||||
AppEnvironment.setCustom(serverBaseURL: apiServerAddress,
|
||||
webAppBaseURL: webServerAddress,
|
||||
ttsBaseURL: ttsServerAddress)
|
||||
dataService.switchAppEnvironment(appEnvironment: AppEnvironment.custom)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ struct HighlightViewer: PlatformViewRepresentable {
|
|||
}
|
||||
|
||||
private func loadContent(webView: WKWebView) {
|
||||
// swiftlint:disable line_length
|
||||
let themeKey = ThemeManager.currentThemeName
|
||||
let content = """
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -73,6 +74,7 @@ struct HighlightViewer: PlatformViewRepresentable {
|
|||
</body>
|
||||
</html>
|
||||
"""
|
||||
// swiftlint:enable line_length
|
||||
|
||||
webView.loadHTMLString(content, baseURL: ViewsPackage.resourceURL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Utils
|
|||
import Views
|
||||
import WebKit
|
||||
|
||||
// swiftlint:disable:next type_body_length
|
||||
// swiftlint:disable file_length type_body_length
|
||||
struct WebReaderContainerView: View {
|
||||
let item: LinkedItem
|
||||
|
||||
|
|
@ -195,6 +195,7 @@ struct WebReaderContainerView: View {
|
|||
viewModel.downloadAudio(audioController: audioController, item: item)
|
||||
},
|
||||
label: {
|
||||
// swiftlint:disable:next line_length
|
||||
Label(viewModel.isDownloadingAudio ? "Downloading Audio" : "Download Audio", systemImage: "icloud.and.arrow.down")
|
||||
}
|
||||
)
|
||||
|
|
@ -554,10 +555,11 @@ struct WebReaderContainerView: View {
|
|||
func scrollToTop() {}
|
||||
|
||||
func openOriginalURL(urlString: String?) {
|
||||
if let urlString = urlString,
|
||||
let url = URL(string: urlString)
|
||||
{
|
||||
openURL(url)
|
||||
}
|
||||
guard
|
||||
let urlString = urlString,
|
||||
let url = URL(string: urlString)
|
||||
else { return }
|
||||
|
||||
openURL(url)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ struct WebReaderContent {
|
|||
"""
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
// swiftlint:disable line_length function_body_length
|
||||
static func emptyContent(isDark: Bool) -> String {
|
||||
let themeKey = isDark ? "Gray" : "LightGray"
|
||||
// let savedAt = "new Date(\(item.unwrappedSavedAt.timeIntervalSince1970 * 1000)).toISOString()"
|
||||
|
|
|
|||
|
|
@ -192,10 +192,11 @@ struct SafariWebLink: Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
func setLabelsForHighlight(highlightID: String,
|
||||
labelIDs: [String],
|
||||
dataService: DataService)
|
||||
{
|
||||
func setLabelsForHighlight(
|
||||
highlightID: String,
|
||||
labelIDs: [String],
|
||||
dataService: DataService
|
||||
) {
|
||||
dataService.setLabelsForHighlight(highlightID: highlightID, labelIDs: labelIDs)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,10 @@ public extension AppEnvironment {
|
|||
case .test, .local:
|
||||
return URL(string: "http://localhost:4000")!
|
||||
case .custom:
|
||||
guard let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.serverBaseURL.rawValue), let url = URL(string: str) else {
|
||||
guard
|
||||
let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.serverBaseURL.rawValue),
|
||||
let url = URL(string: str)
|
||||
else {
|
||||
fatalError("custom serverBaseURL not set")
|
||||
}
|
||||
return url
|
||||
|
|
@ -74,7 +77,10 @@ public extension AppEnvironment {
|
|||
case .test, .local:
|
||||
return URL(string: "http://localhost:3000")!
|
||||
case .custom:
|
||||
guard let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.webAppBaseURL.rawValue), let url = URL(string: str) else {
|
||||
guard
|
||||
let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.webAppBaseURL.rawValue),
|
||||
let url = URL(string: str)
|
||||
else {
|
||||
fatalError("custom webAppBaseURL not set")
|
||||
}
|
||||
return url
|
||||
|
|
@ -90,7 +96,10 @@ public extension AppEnvironment {
|
|||
case .test, .local:
|
||||
return URL(string: "http://localhost:4000")!
|
||||
case .custom:
|
||||
guard let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.ttsBaseURL.rawValue), let url = URL(string: str) else {
|
||||
guard
|
||||
let str = UserDefaults.standard.string(forKey: AppEnvironmentUserDefaultKey.ttsBaseURL.rawValue),
|
||||
let url = URL(string: str)
|
||||
else {
|
||||
fatalError("custom ttsBaseURL not set")
|
||||
}
|
||||
return url
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public class PersistentContainer: NSPersistentContainer {
|
|||
#endif
|
||||
let appGroupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)
|
||||
let appGroupContainerURL = appGroupContainer?.appendingPathComponent("store.sqlite")
|
||||
print("starting with sqlite container", appGroupContainerURL)
|
||||
logger.debug("starting with sqlite container \(appGroupContainerURL?.absoluteString ?? "(nil)")")
|
||||
|
||||
container.persistentStoreDescriptions.first!.url = appGroupContainerURL
|
||||
|
||||
|
|
|
|||
|
|
@ -151,12 +151,14 @@ public extension LinkedItem {
|
|||
"userID": NSString(string: recommendation.user?.userID ?? ""),
|
||||
"name": NSString(string: recommendation.user?.name ?? ""),
|
||||
"username": NSString(string: recommendation.user?.username ?? ""),
|
||||
"profileImageURL": recommendation.user?.profileImageURL == nil ? nil : NSString(string: recommendation.user?.profileImageURL ?? "")
|
||||
"profileImageURL": recommendation.user?.profileImageURL as NSString? as Any
|
||||
]),
|
||||
"recommendedAt": recommendedAt == nil ? nil : NSString(string: recommendedAt!)
|
||||
]
|
||||
}
|
||||
guard let JSON = (try? JSONSerialization.data(withJSONObject: recommendations, options: .prettyPrinted)) else { return "[]" }
|
||||
guard let JSON = try? JSONSerialization.data(withJSONObject: recommendations, options: .prettyPrinted) else {
|
||||
return "[]"
|
||||
}
|
||||
return String(data: JSON, encoding: .utf8) ?? "[]"
|
||||
}
|
||||
|
||||
|
|
@ -203,6 +205,7 @@ public extension LinkedItem {
|
|||
return item
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func update(
|
||||
inContext context: NSManagedObjectContext,
|
||||
newReadingProgress: Double? = nil,
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ public struct HighlightData {
|
|||
public let highlightText: String
|
||||
|
||||
public static func make(dict: NSDictionary?) -> HighlightData? {
|
||||
if let dict = dict,
|
||||
let highlightHTML = dict["highlightHTML"] as? String,
|
||||
let highlightText = dict["highlightText"] as? String
|
||||
if
|
||||
let dict = dict,
|
||||
let highlightHTML = dict["highlightHTML"] as? String,
|
||||
let highlightText = dict["highlightText"] as? String
|
||||
{
|
||||
return HighlightData(highlightHTML: highlightHTML, highlightText: highlightText)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Models
|
|||
|
||||
// Somewhat based on: https://github.com/neekeetab/CachingPlayerItem/blob/master/CachingPlayerItem.swift
|
||||
class SpeechPlayerItem: AVPlayerItem {
|
||||
let resourceLoaderDelegate = ResourceLoaderDelegate()
|
||||
let resourceLoaderDelegate = ResourceLoaderDelegate() // swiftlint:disable:this weak_delegate
|
||||
let session: AudioController
|
||||
let speechItem: SpeechItem
|
||||
var speechMarks: [SpeechMark]?
|
||||
|
|
@ -22,6 +22,7 @@ class SpeechPlayerItem: AVPlayerItem {
|
|||
|
||||
var observer: Any?
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
init(session: AudioController, prefetchQueue: OperationQueue, speechItem: SpeechItem, completed: @escaping () -> Void) {
|
||||
self.speechItem = speechItem
|
||||
self.session = session
|
||||
|
|
@ -84,9 +85,10 @@ class SpeechPlayerItem: AVPlayerItem {
|
|||
var pendingRequests = Set<AVAssetResourceLoadingRequest>()
|
||||
weak var owner: SpeechPlayerItem?
|
||||
|
||||
func resourceLoader(_: AVAssetResourceLoader,
|
||||
shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool
|
||||
{
|
||||
func resourceLoader(
|
||||
_: AVAssetResourceLoader,
|
||||
shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest
|
||||
) -> Bool {
|
||||
if owner == nil {
|
||||
return true
|
||||
}
|
||||
|
|
@ -119,6 +121,7 @@ class SpeechPlayerItem: AVPlayerItem {
|
|||
}
|
||||
|
||||
do {
|
||||
// swiftlint:disable:next line_length
|
||||
let speechData = try await SpeechSynthesizer.download(speechItem: speechItem, session: self.session ?? URLSession.shared)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
|
@ -172,6 +175,7 @@ class SpeechPlayerItem: AVPlayerItem {
|
|||
_ = requestsFulfilled.map { self.pendingRequests.remove($0) }
|
||||
}
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
func fillInContentInformationRequest(_ contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
|
||||
contentInformationRequest?.contentType = UTType.mp3.identifier
|
||||
|
||||
|
|
|
|||
|
|
@ -174,13 +174,13 @@ struct SpeechSynthesizer {
|
|||
|
||||
static func downloadData(session: URLSession, request: URLRequest) async throws -> Data {
|
||||
do {
|
||||
let result: (Data, URLResponse)? = try await session.data(for: request)
|
||||
guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
|
||||
print("error: ", result?.1)
|
||||
let (data, response) = try await session.data(for: request)
|
||||
guard let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
|
||||
print("error: ", response)
|
||||
throw BasicError.message(messageText: "audioFetch failed. no response or bad status code.")
|
||||
}
|
||||
|
||||
guard let data = result?.0 else {
|
||||
guard !data.isEmpty else {
|
||||
throw BasicError.message(messageText: "audioFetch failed. no data received.")
|
||||
}
|
||||
|
||||
|
|
@ -194,10 +194,11 @@ struct SpeechSynthesizer {
|
|||
}
|
||||
}
|
||||
|
||||
static func download(speechItem: SpeechItem,
|
||||
redownloadCached: Bool = false,
|
||||
session: URLSession = URLSession.shared) async throws -> SynthesizeData?
|
||||
{
|
||||
static func download(
|
||||
speechItem: SpeechItem,
|
||||
redownloadCached: Bool = false,
|
||||
session: URLSession = URLSession.shared
|
||||
) async throws -> SynthesizeData? {
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
if !redownloadCached {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public struct VoicePair {
|
|||
let category: VoiceCategory
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
public enum Voices {
|
||||
public static func isUltraRealisticVoice(_ voiceKey: String) -> Bool {
|
||||
UltraPairs.contains(where: { voice in
|
||||
|
|
@ -77,7 +78,6 @@ public enum Voices {
|
|||
VoiceLanguage(key: "ta", name: "Tamil", defaultVoice: "ta-IN-PallaviNeural", categories: [.taIN, .taLK, .taMY, .taSG])
|
||||
]
|
||||
|
||||
// swiftlint:disable all
|
||||
public static let Pairs = [
|
||||
VoicePair(firstKey: "en-US-JennyNeural", secondKey: "en-US-BrandonNeural", firstName: "Jenny", secondName: "Brandon", language: "en-US", category: .enUS),
|
||||
VoicePair(firstKey: "en-US-CoraNeural", secondKey: "en-US-ChristopherNeural", firstName: "Cora", secondName: "Christopher", language: "en-US", category: .enUS),
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ public final class DataService: ObservableObject {
|
|||
persistentContainer.viewContext
|
||||
}
|
||||
|
||||
@AppStorage(UserDefaultKey.lastItemSyncTime.rawValue) public var lastItemSyncTime = DateFormatter.formatterISO8601.string(
|
||||
from: Date(timeIntervalSinceReferenceDate: 0)
|
||||
)
|
||||
@AppStorage(UserDefaultKey.lastItemSyncTime.rawValue) public var lastItemSyncTime: String = {
|
||||
DateFormatter.formatterISO8601.string(from: Date(timeIntervalSinceReferenceDate: 0))
|
||||
}()
|
||||
|
||||
public init(appEnvironment: AppEnvironment, networker: Networker) {
|
||||
self.appEnvironment = appEnvironment
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Models
|
|||
import SwiftGraphQL
|
||||
|
||||
extension DataService {
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
public func createHighlight(
|
||||
shortId: String,
|
||||
highlightID: String,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Models
|
|||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
// swiftlint:disable:next line_length
|
||||
func createRecommendationGroup(name: String, onlyAdminCanPost: Bool, onlyAdminCanSeeMembers: Bool) async throws -> InternalRecommendationGroup {
|
||||
enum MutationResult {
|
||||
case saved(recommendationGroup: InternalRecommendationGroup)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ public extension DataService {
|
|||
case error(errorCode: Enums.OptInFeatureErrorCode)
|
||||
}
|
||||
|
||||
let featureSelection = Selection.Feature { Feature(name: try $0.name(), token: try $0.token(), granted: try $0.grantedAt() != nil) }
|
||||
let featureSelection = Selection.Feature {
|
||||
Feature(name: try $0.name(), token: try $0.token(), granted: try $0.grantedAt() != nil)
|
||||
}
|
||||
let selection = Selection<MutationResult, Unions.OptInFeatureResult> {
|
||||
try $0.on(
|
||||
optInFeatureError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ public extension DataService {
|
|||
|
||||
let mutation = Selection.Mutation {
|
||||
try $0.recommend(
|
||||
input: .init(groupIds: groupIDs, note: OptionalArgument(note), pageId: pageID, recommendedWithHighlights: OptionalArgument(withHighlights)),
|
||||
input: .init(groupIds: groupIDs,
|
||||
note: OptionalArgument(note),
|
||||
pageId: pageID,
|
||||
recommendedWithHighlights: OptionalArgument(withHighlights)),
|
||||
selection: selection
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@ extension DataService {
|
|||
|
||||
let mutation = Selection.Mutation {
|
||||
try $0.updatePage(
|
||||
input: .init(byline: OptionalArgument(author), description: OptionalArgument(description), pageId: itemID, title: OptionalArgument(title)),
|
||||
input: .init(byline: OptionalArgument(author),
|
||||
description: OptionalArgument(description),
|
||||
pageId: itemID,
|
||||
title: OptionalArgument(title)),
|
||||
selection: selection
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ public extension DataService {
|
|||
|
||||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
let context = backgroundContext
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
send(query, to: path, headers: headers) { queryResult in
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ extension DataService {
|
|||
)
|
||||
}
|
||||
|
||||
let sort = InputObjects.SortParams(by: Enums.SortBy.updatedTime, order: OptionalArgument(descending ? Enums.SortOrder.descending : Enums.SortOrder.ascending))
|
||||
let sort = InputObjects.SortParams(by: .updatedTime,
|
||||
order: OptionalArgument(descending ? .descending : .ascending))
|
||||
|
||||
let query = Selection.Query {
|
||||
try $0.updatesSince(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ public struct TypeaheadSearchItem: Identifiable {
|
|||
}
|
||||
|
||||
public extension DataService {
|
||||
// swiftlint:disable:next function_body_length
|
||||
func typeaheadSearch(searchTerm: String) async throws -> [TypeaheadSearchItem] {
|
||||
enum QueryResult {
|
||||
case success(result: [TypeaheadSearchItem])
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ public struct InternalRecommendation {
|
|||
public static func make(_ recommendations: NSSet?) -> [InternalRecommendation] {
|
||||
recommendations?
|
||||
.compactMap { recommendation in
|
||||
if let recommendation = recommendation as? Recommendation,
|
||||
let groupID = recommendation.groupID,
|
||||
let name = recommendation.name,
|
||||
let recommendedAt = recommendation.recommendedAt
|
||||
if
|
||||
let recommendation = recommendation as? Recommendation,
|
||||
let groupID = recommendation.groupID,
|
||||
let name = recommendation.name,
|
||||
let recommendedAt = recommendation.recommendedAt
|
||||
{
|
||||
return InternalRecommendation(
|
||||
groupID: groupID,
|
||||
|
|
|
|||
|
|
@ -36,9 +36,10 @@ public struct InternalRecommendationGroup: Identifiable {
|
|||
}
|
||||
|
||||
public static func make(from recommendationGroup: RecommendationGroup) -> InternalRecommendationGroup? {
|
||||
if let id = recommendationGroup.id,
|
||||
let name = recommendationGroup.name,
|
||||
let inviteUrl = recommendationGroup.inviteUrl
|
||||
if
|
||||
let id = recommendationGroup.id,
|
||||
let name = recommendationGroup.name,
|
||||
let inviteUrl = recommendationGroup.inviteUrl
|
||||
{
|
||||
return InternalRecommendationGroup(
|
||||
id: id,
|
||||
|
|
|
|||
|
|
@ -43,37 +43,39 @@ public struct InternalUserProfile: Identifiable, Encodable {
|
|||
}
|
||||
|
||||
public static func makeSingle(_ user: UserProfile?) -> InternalUserProfile? {
|
||||
if let user = user,
|
||||
let userID = user.userID,
|
||||
let name = user.name,
|
||||
let username = user.username
|
||||
{
|
||||
return InternalUserProfile(
|
||||
userID: userID,
|
||||
name: name,
|
||||
username: username,
|
||||
profileImageURL: user.profileImageURL
|
||||
)
|
||||
guard
|
||||
let user = user,
|
||||
let userID = user.userID,
|
||||
let name = user.name,
|
||||
let username = user.username
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return InternalUserProfile(
|
||||
userID: userID,
|
||||
name: name,
|
||||
username: username,
|
||||
profileImageURL: user.profileImageURL
|
||||
)
|
||||
}
|
||||
|
||||
public static func make(_ users: NSSet?) -> [InternalUserProfile] {
|
||||
users?
|
||||
.compactMap { user in
|
||||
if let user = user as? UserProfile,
|
||||
let userID = user.userID,
|
||||
let name = user.name,
|
||||
let username = user.username
|
||||
{
|
||||
return InternalUserProfile(
|
||||
userID: userID,
|
||||
name: name,
|
||||
username: username,
|
||||
profileImageURL: user.profileImageURL
|
||||
)
|
||||
guard
|
||||
let user = user as? UserProfile,
|
||||
let userID = user.userID,
|
||||
let name = user.name,
|
||||
let username = user.username
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return InternalUserProfile(
|
||||
userID: userID,
|
||||
name: name,
|
||||
username: username,
|
||||
profileImageURL: user.profileImageURL
|
||||
)
|
||||
} ?? []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import UIKit
|
||||
|
||||
public extension UIViewController {
|
||||
// swiftlint:disable line_length
|
||||
func embed(childViewController child: UIViewController, heightRatio: CGFloat? = nil) {
|
||||
addChild(child)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
|
||||
// swiftlint:disable identifier_name
|
||||
public enum UserDefaultKey: String {
|
||||
case preferredWebFont
|
||||
case preferredWebFontSize
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Models
|
||||
import Utils
|
||||
import WebKit
|
||||
// swiftlint:disable file_length
|
||||
|
||||
/// Describes actions that can be sent from the WebView back to native views.
|
||||
/// The names on the javascript side must match for an action to be handled.
|
||||
|
|
@ -273,6 +274,7 @@ public final class OmnivoreWebView: WKWebView {
|
|||
true
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
override public func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool {
|
||||
switch action {
|
||||
case #selector(annotateSelection): return true
|
||||
|
|
@ -351,13 +353,18 @@ public final class OmnivoreWebView: WKWebView {
|
|||
override public func buildMenu(with builder: UIMenuBuilder) {
|
||||
if #available(iOS 16.0, *) {
|
||||
let annotate = UICommand(title: "Note", action: #selector(annotateSelection))
|
||||
let highlight = UICommand(title: LocalText.genericHighlight, action: #selector(highlightSelection))
|
||||
let remove = UICommand(title: "Remove", action: #selector(removeSelection))
|
||||
let setLabels = UICommand(title: LocalText.labelsGeneric, action: #selector(setLabels))
|
||||
|
||||
let omnivore = UIMenu(title: "",
|
||||
options: .displayInline,
|
||||
children: currentMenu == .defaultMenu ? [highlight, annotate] : [annotate, setLabels, remove])
|
||||
let items: [UIMenuElement]
|
||||
if currentMenu == .defaultMenu {
|
||||
let highlight = UICommand(title: LocalText.genericHighlight, action: #selector(highlightSelection))
|
||||
items = [highlight, annotate]
|
||||
} else {
|
||||
let remove = UICommand(title: "Remove", action: #selector(removeSelection))
|
||||
let setLabels = UICommand(title: LocalText.labelsGeneric, action: #selector(setLabels))
|
||||
items = [annotate, setLabels, remove]
|
||||
}
|
||||
|
||||
let omnivore = UIMenu(title: "", options: .displayInline, children: items)
|
||||
builder.insertSibling(omnivore, beforeMenu: .lookup)
|
||||
}
|
||||
|
||||
|
|
@ -418,6 +425,7 @@ public enum WebViewDispatchEvent {
|
|||
case saveReadPosition
|
||||
|
||||
var script: String {
|
||||
// swiftlint:disable:next implicit_getter
|
||||
get throws {
|
||||
let propertyLine = try scriptPropertyLine
|
||||
return "var event = new Event('\(eventName)');\(propertyLine)document.dispatchEvent(event);"
|
||||
|
|
@ -470,6 +478,7 @@ public enum WebViewDispatchEvent {
|
|||
}
|
||||
|
||||
private var scriptPropertyLine: String {
|
||||
// swiftlint:disable:next implicit_getter
|
||||
get throws {
|
||||
switch self {
|
||||
case let .handleFontContrastChange(isHighContrast: isHighContrast):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Foundation
|
|||
import StoreKit
|
||||
import SwiftUI
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
let tweetUrl = "https://twitter.com/intent/tweet?text=I%20recently%20started%20using%20@OmnivoreApp%20as%20a%20free,%20open-source%20read-it-later%20app.%20Check%20it%20out:%20https://omnivore.app"
|
||||
|
||||
public struct CommunityModal: View {
|
||||
|
|
@ -40,9 +41,9 @@ public struct CommunityModal: View {
|
|||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
Button {
|
||||
dismiss()
|
||||
}) {
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle")
|
||||
.padding(10)
|
||||
}
|
||||
|
|
@ -80,6 +81,7 @@ public struct CommunityModal: View {
|
|||
var buttonLinks: some View {
|
||||
VStack(spacing: 15) {
|
||||
Button(action: {
|
||||
// swiftlint:disable:next line_length
|
||||
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
||||
SKStoreReviewController.requestReview(in: scene)
|
||||
}
|
||||
|
|
@ -99,8 +101,9 @@ public struct CommunityModal: View {
|
|||
VStack(spacing: 0) {
|
||||
header
|
||||
|
||||
Text((try? AttributedString(markdown: message,
|
||||
options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace))) ?? "")
|
||||
let parsedMessage = try? AttributedString(markdown: message,
|
||||
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace))
|
||||
Text(parsedMessage ?? "")
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(Color.appGrayTextContrast)
|
||||
.accentColor(.blue)
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ public struct FeaturePrimer: View {
|
|||
}
|
||||
|
||||
ScrollView {
|
||||
Text((try? AttributedString(markdown: message,
|
||||
options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace))) ?? "")
|
||||
let parsedMessage = try? AttributedString(markdown: message,
|
||||
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace))
|
||||
Text(parsedMessage ?? "")
|
||||
.foregroundColor(Color.appGrayText)
|
||||
.accentColor(.blue)
|
||||
.padding(.bottom, 16)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import SwiftUI
|
|||
.font(.appHeadline)
|
||||
}
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
Text(LocalText.notificationsGeneralExplainer)
|
||||
.font(.appBody)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
|
|
|||
|
|
@ -198,10 +198,6 @@
|
|||
"errorNetwork" = "我们在连接到互联网时遇到问题。";
|
||||
"documentationGeneric" = "使用文档";
|
||||
|
||||
// Unit test Entry -- Do not remove this or add entries after this one.
|
||||
// This allows us to check for syntax errors in this file with a unit test
|
||||
"unitTestTrailingEntry" = "仅用于测试目的。";
|
||||
|
||||
// Unreferenced
|
||||
// "signInScreenHeadline" = "保存并分享喜爱的网页.";
|
||||
// "registrationViewSignInHeadline" = "登录";
|
||||
|
|
@ -287,7 +283,7 @@
|
|||
//"Invite link copied" = "已复制邀请链接";
|
||||
//
|
||||
//// Views/Profile/RecommendationGroupsView.swift
|
||||
//"Error creating club"
|
||||
//"Error creating club" = "Error creating club";
|
||||
//"Club Rules" = "已复制邀请链接";
|
||||
//"Only admins can post" = "只有管理员可以发帖";
|
||||
//"Only admins can see members" = "只有管理员才能看到成员";
|
||||
|
|
@ -330,3 +326,7 @@
|
|||
//"View Privacy Policy" = "查看隐私策略";
|
||||
//"Self-hosting options" = "自我管选项";
|
||||
//"View Details" = "查看详情";
|
||||
|
||||
// Unit test Entry -- Do not remove this or add entries after this one.
|
||||
// This allows us to check for syntax errors in this file with a unit test
|
||||
"unitTestTrailingEntry" = "仅用于测试目的。";
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public struct TextChip: View {
|
|||
self.padded = false
|
||||
}
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
public init?(feedItemLabel: LinkedItemLabel, negated: Bool = false, checked: Bool = false, padded: Bool = false, onTap: ((TextChip) -> Void)?) {
|
||||
guard let color = Color(hex: feedItemLabel.color ?? "") else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -40,12 +40,7 @@ public enum Theme: String, CaseIterable {
|
|||
}
|
||||
|
||||
public static func fromName(themeName: String) -> Theme? {
|
||||
for theme in Theme.allCases {
|
||||
if theme.rawValue == themeName {
|
||||
return theme
|
||||
}
|
||||
}
|
||||
return nil
|
||||
Theme.allCases.first(where: { $0.rawValue == themeName })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import XCTest
|
|||
final class LocalTextTests: XCTestCase {
|
||||
func testThatLocalTextFindsStrings() {
|
||||
// Make sure that the same key is not returned when looking up a localized string by key
|
||||
// Testing the first and last entry in teh strings file is adequate for finding syntax errors.
|
||||
// Testing the first and last entry in the strings file is adequate for finding syntax errors.
|
||||
// If any entry is not proper than the key will be returned and the test will fail.
|
||||
|
||||
for languageCode in LanguageCode.allCases {
|
||||
XCTAssertNotEqual(languageCode.translation(key: "unitTestLeadingEntry"), "unitTestLeadingEntry")
|
||||
XCTAssertNotEqual(languageCode.translation(key: "unitTestTrailingEntry"), "unitTestTrailingEntry")
|
||||
// swiftlint:disable line_length
|
||||
XCTAssertNotEqual(languageCode.translation(key: "unitTestLeadingEntry"), "unitTestLeadingEntry", "Got untranslated value for unitTestLeadingEntry in \(languageCode)")
|
||||
XCTAssertNotEqual(languageCode.translation(key: "unitTestTrailingEntry"), "unitTestTrailingEntry", "Got untranslated value for unitTestTrailingEntry in \(languageCode)")
|
||||
// swiftlint:enable line_length
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ private let logger = Logger(subsystem: "app.omnivore", category: "app-delegate")
|
|||
Services.registerBackgroundFetch()
|
||||
configureFirebase()
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
NotificationCenter.default.addObserver(forName: Notification.Name("ReconfigurePushNotifications"), object: nil, queue: OperationQueue.main) { _ in
|
||||
if UserDefaults.standard.bool(forKey: UserDefaultKey.notificationsEnabled.rawValue) {
|
||||
self.registerForNotifications()
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ extension AppDelegate {
|
|||
return firebaseKeys ?? AppKeys.sharedInstance?.firebaseProdKeys
|
||||
}()
|
||||
|
||||
guard let keys = keys else { return }
|
||||
guard let keys = keys, !keys.googleAppID.isEmpty else { return }
|
||||
|
||||
let firebaseOpts = FirebaseOptions(googleAppID: keys.googleAppID, gcmSenderID: keys.gcmSenderID)
|
||||
|
||||
|
|
|
|||
160
docs/guides/getting-started/cn/getting-started-guide.md
Normal file
160
docs/guides/getting-started/cn/getting-started-guide.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Getting Started with Omnivore
|
||||
|
||||
Omnivore 是一个**稍后阅读应用** ,让你保存和整理你在网上阅读的所有内容。
|
||||
|
||||
该指南将向您展示如何使用 Omnivore 的基本功能和高级特性,分为四个主要功能:
|
||||
|
||||
* 保存
|
||||
* 阅读
|
||||
* 整理
|
||||
* 集成
|
||||
|
||||
**文库**是您 Omnivore 体验的中心,您可以在其中快速访问已保存的任何链接。保存的链接将永久保留在您的文库中,除非您将其删除。
|
||||
|
||||
## 保存
|
||||
|
||||
有五种主要方法可以保存您希望稍后阅读的页面或文章的链接:
|
||||
|
||||
* 从 Omnivore 文库保存
|
||||
* 从浏览器保存
|
||||
* 从手机或平板电脑 (iOS 或 Android)
|
||||
* 通过电子邮件订阅新闻稿件
|
||||
* 从 Mac 保存PDF
|
||||
|
||||
### 从 Omnivore 文库中保存
|
||||
1. 在资料库的右上角,点击 **添加链接** 按钮。
|
||||
2. 输入您想要保存的 URL 超链接,然后点击**添加链接**。
|
||||
3. 下次刷新链接时,该链接将显示在您的文库中。
|
||||
|
||||
### 从浏览器保存
|
||||
|
||||
1. 下载并安装浏览器的 Omnivore 扩展程序:
|
||||
|
||||
* [Chrome ](https://omnivore.app/install/chrome)
|
||||
* [Edge](https://omnivore.app/install/edge)
|
||||
* [Firefox](https://omnivore.app/install/firefox)
|
||||
* [Safari](https://omnivore.app/install/safari)
|
||||
|
||||
2. 导航到您要保存的页面,然后点击浏览器工具栏或扩展菜单中的 Omnivore 按钮。
|
||||
3. 或者,您可以在任何超链接上右键单击(Mac:command+click),然后从菜单中选择 **保存到Omnivore**。
|
||||
4. 下次刷新该链接时,该链接将显示在您的文库中。
|
||||
|
||||
### 从手机或平板电脑保存
|
||||
|
||||
从移动设备保存链接的最佳方法是通过 Omnivore 应用程序。您可以在此处下载此应用程序:
|
||||
|
||||
* [iOS (iPhone 或 iPad)](https://omnivore.app/install/ios)
|
||||
* Android
|
||||
|
||||
安装移动应用程序后:
|
||||
|
||||
1. 在浏览器中,导航到您要保存的页面,然后点击 **共享** 按钮。
|
||||
2. 点击共享菜单中的 **Omnivore** 图标。
|
||||
3. 下次刷新链接时,该链接将显示在您的文库中。
|
||||
|
||||
### 通过电子邮件订阅新闻稿件
|
||||
|
||||
1. 在 Omnivore 网站或应用程序上,点击右上角的照片、首字母或头像以访问个人资料菜单。从菜单中选择 **电子邮件**。
|
||||
2. 点击**创建新电子邮件地址**将新电子邮件地址 (例如: username-123_abc@inbox.omnivore.app) 添加到列表中。
|
||||
3. 点按电子邮件地址旁边的拷贝图标。
|
||||
4. 导航到您想要订阅的新闻稿件的注册页面。
|
||||
5. 将 Omnivore 电子邮件地址粘贴到注册表中。
|
||||
6. 新的新闻稿件将自动发送到您的 Omnivore 收集箱。
|
||||
|
||||
### 从 Mac 保存PDF
|
||||
|
||||
1. 安装 [Mac 应用程序](https://omnivore.app/install/mac)。
|
||||
2. 在 Mac 上,找到要保存的PDF,然后右键单击或 ctrl + 单击 文件名。
|
||||
3. 从菜单中选择 **共享**,然后选择 **Omnivore**。
|
||||
4. 下次刷新链接时,该链接将显示在您的文库中。
|
||||
|
||||
## 阅读
|
||||
|
||||
单击保存在文库中的任何链接以进入阅读器视图。
|
||||
|
||||
Omnivore 格式页面,便于阅读和高亮显示,没有广告和杂乱的排版,阅读不分心。以文本为中心的视图还可以使文章更小、加载更快。
|
||||
|
||||
在阅读时,您可以:
|
||||
|
||||
* <span style="text-decoration:underline;">更改样式</span>
|
||||
* <span style="text-decoration:underline;">高亮显示文本</span>
|
||||
* <span style="text-decoration:underline;">添加注释</span>
|
||||
* <span style="text-decoration:underline;">查看所有保存的高亮显示和注释</span>
|
||||
* <span style="text-decoration:underline;">跟踪阅读进度</span>
|
||||
|
||||
### 更改样式
|
||||
|
||||
1. **_主题:_** 点击右上角的照片,首字母或头像以访问个人资料菜单。选择白色或黑色缩略图以选择浅色或深色主题。
|
||||
2. **_文本样式:_** 点击 Aa 图标以调整文本大小、字体、边距和行距。
|
||||
|
||||
### 高亮显示文本
|
||||
|
||||
1. 选择要高亮显示的文本内容。
|
||||
2. 点击 **荧光笔** <sup>(Highlight)</sup>按钮。
|
||||
3. 下次查看文章时,文本将高亮显示。
|
||||
|
||||
### 添加注释
|
||||
|
||||
1. 高亮显示要添加注释的文本部分。
|
||||
2. 点击 **注释** <sup>(Note)</sup>按钮,键入您的注释,然后点击 **保存**.
|
||||
3. 下次查看本文时将显示“注释”图标。
|
||||
|
||||
### 查看所有保存的高亮显示和注释
|
||||
|
||||
1. 点击 荧光笔/注释 图标以查看已添加到此页面的所有荧光笔高亮显示文本和注释的列表。
|
||||
2. 要删除注释或荧光笔高亮显示,请从列表中选择它,然后点击废纸篓图标。
|
||||
|
||||
### 跟踪阅读进度
|
||||
|
||||
Omnivore 自动跟踪您在不同设备上的阅读进度,以便您可以轻松地从上次中断的地方继续。开始阅读后,文库中每个链接的顶部都会显示一个进度条。
|
||||
|
||||
## 整理
|
||||
|
||||
默认情况下,文库中的收集箱显示已保存的所有链接。为了管理您的列表并保持阅读井井有条,Omnivore 提供了以下操作:
|
||||
|
||||
* <span style="text-decoration:underline;">存档</span>
|
||||
* <span style="text-decoration:underline;">标签</span>
|
||||
* <span style="text-decoration:underline;">搜索</span>
|
||||
* <span style="text-decoration:underline;">筛选器 (过滤器)</span>
|
||||
|
||||
### 存档
|
||||
|
||||
1. 点击要存档的链接旁边的菜单图标(在移动应用程序上,长按链接以打开菜单)。
|
||||
2. 选择 **存档**。
|
||||
3. 该链接将从默认的“文库”视图中消失,但如果选择“存档”筛选器,则会显示该链接 (请参阅下面的 <span style="text-decoration:underline;">筛选器 (过滤器) </span>)。
|
||||
|
||||
### 标签
|
||||
|
||||
1. 点击任何链接旁边的菜单图标,然后选择 **设置标签**。
|
||||
2. 从列表中选择现有标签,或点击 **编辑标签** 以创建新标签。
|
||||
3. 标签将显示在文库中的链接旁边。点击它以查看具有相同标签的所有链接。
|
||||
4. _仅限 Omnivore 移动应用_:点按 **标签** 以查看您使用过的所有标签的完整列表;点击其中一个可查看具有相同标签的所有链接
|
||||
5. 注释:会自动分配一些标签,例如“新闻稿件”。
|
||||
|
||||
### 搜索
|
||||
|
||||
1. 要搜索所有已保存的链接,请在搜索栏中输入关键字或短语。
|
||||
2. 您可以将关键字与标签和筛选器结合使用,以进一步集中搜索。 [了解有关高级搜索的更多信息](https://omnivore.app/help/search)。
|
||||
|
||||
### 筛选器 (过滤器)
|
||||
|
||||
1. 使用 **筛选器** <sup>(Filters)</sup>菜单以优化您的文库视图 (默认某些情况下筛选器可能可见).
|
||||
2. 选择 **稍后阅读** <sup>(Read Later)</sup>以查看除新闻稿件之外的所有未存档链接的列表。
|
||||
3. 选择 **荧光笔** <sup>(Highlights)</sup>以查看您在所有已保存页面中高亮显示的文本选择。
|
||||
4. 选择 **今天** <sup>(Today)</sup>以查看您今天保存的链接列表。
|
||||
5. 选择 **新闻稿件** <sup>(Newsletters)</sup>以查看通过新闻稿件订阅保存的链接。
|
||||
|
||||
## 集成
|
||||
|
||||
Omnivore 允许与知识库和笔记应用程序集成,包括:
|
||||
|
||||
* Logseq
|
||||
* Webhooks
|
||||
|
||||
### Logseq
|
||||
|
||||
使用 Omnivore 的 Logseq 插件,您可以将所有保存的文章,荧光笔高亮显示和注释同步到 Logseq,一个流行的知识库中。有关设置和使用 Logseq 插件的信息,请参阅这个有用的 [Omnivore for Logseq 插件指南](https://briansunter.com/graph/#/page/omnivore-logseq-guide)。
|
||||
|
||||
### Webhooks
|
||||
|
||||
Omnivore 可以在您保存链接或向您正在阅读的页面添加突出显示时触发Webhooks。 <span style="text-decoration:underline;">该示例</span> 展示了 Webhook 被用来将所有保存的链接存储到 Google Drive 上的 Google Sheet 电子表格。
|
||||
|
|
@ -686,6 +686,7 @@ export const updatePagesAsync = async (
|
|||
state: [
|
||||
ArticleSavingRequestStatus.Succeeded,
|
||||
ArticleSavingRequestStatus.Failed,
|
||||
ArticleSavingRequestStatus.Processing,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import {
|
|||
validatedDate,
|
||||
} from '../../utils/helpers'
|
||||
import {
|
||||
getDistillerResult,
|
||||
htmlToMarkdown,
|
||||
ParsedContentPuppeteer,
|
||||
parsePreparedContent,
|
||||
|
|
@ -106,6 +107,12 @@ import { saveSearchHistory } from '../../services/search_history'
|
|||
import { parsedContentToPage } from '../../services/save_page'
|
||||
import * as httpContext from 'express-http-context'
|
||||
|
||||
enum ArticleFormat {
|
||||
Markdown = 'markdown',
|
||||
Html = 'html',
|
||||
Distiller = 'distiller',
|
||||
}
|
||||
|
||||
export type PartialArticle = Omit<
|
||||
Article,
|
||||
| 'updatedAt'
|
||||
|
|
@ -413,7 +420,9 @@ export const getArticleResolver: ResolverFn<
|
|||
return { errorCodes: [ArticleErrorCode.Unauthorized] }
|
||||
}
|
||||
|
||||
const includeOriginalHtml = !!graphqlFields(info).article.originalHtml
|
||||
const includeOriginalHtml =
|
||||
format === ArticleFormat.Distiller ||
|
||||
!!graphqlFields(info).article.originalHtml
|
||||
|
||||
analytics.track({
|
||||
userId: claims?.uid,
|
||||
|
|
@ -449,8 +458,20 @@ export const getArticleResolver: ResolverFn<
|
|||
page.content = UNPARSEABLE_CONTENT
|
||||
}
|
||||
|
||||
if (format === 'markdown') {
|
||||
if (format === ArticleFormat.Markdown) {
|
||||
page.content = htmlToMarkdown(page.content)
|
||||
} else if (format === ArticleFormat.Distiller) {
|
||||
if (!page.originalHtml) {
|
||||
return { errorCodes: [ArticleErrorCode.BadData] }
|
||||
}
|
||||
const distillerResult = await getDistillerResult(
|
||||
claims.uid,
|
||||
page.originalHtml
|
||||
)
|
||||
if (!distillerResult) {
|
||||
return { errorCodes: [ArticleErrorCode.BadData] }
|
||||
}
|
||||
page.content = distillerResult
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -73,8 +73,6 @@ export function newsletterServiceRouter() {
|
|||
console.log('create')
|
||||
|
||||
const { message, expired } = readPushSubscription(req)
|
||||
console.log('pubsub message:', message, 'expired:', expired)
|
||||
|
||||
if (!message) {
|
||||
res.status(400).send('Bad Request')
|
||||
return
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export const createPageSaveRequest = async (
|
|||
url: normalizedUrl,
|
||||
})
|
||||
if (page) {
|
||||
console.log('Page already exists', page)
|
||||
console.log('Page already exists', page.id, page.url)
|
||||
articleSavingRequestId = page.id
|
||||
} else {
|
||||
page = {
|
||||
|
|
|
|||
|
|
@ -21,8 +21,12 @@ import {
|
|||
findEmbeddedHighlight,
|
||||
} from './highlightGenerator'
|
||||
import { NodeHtmlMarkdown } from 'node-html-markdown'
|
||||
import { promisify } from 'util'
|
||||
import * as jwt from 'jsonwebtoken'
|
||||
import { env } from '../env'
|
||||
|
||||
const logger = buildLogger('utils.parse')
|
||||
const signToken = promisify(jwt.sign)
|
||||
|
||||
export const ALLOWED_CONTENT_TYPES = [
|
||||
'text/html',
|
||||
|
|
@ -199,7 +203,7 @@ export const parsePreparedContent = async (
|
|||
return {
|
||||
canonicalUrl: url,
|
||||
parsedContent: null,
|
||||
domContent: preparedDocument.document,
|
||||
domContent: document,
|
||||
pageType: PageType.Unknown,
|
||||
}
|
||||
}
|
||||
|
|
@ -219,7 +223,7 @@ export const parsePreparedContent = async (
|
|||
if (!article?.textContent && allowRetry) {
|
||||
const newDocument = {
|
||||
...preparedDocument,
|
||||
document: '<html>' + preparedDocument.document + '</html>',
|
||||
document: '<html>' + document + '</html>',
|
||||
}
|
||||
return parsePreparedContent(
|
||||
url,
|
||||
|
|
@ -235,7 +239,9 @@ export const parsePreparedContent = async (
|
|||
// to the handlers, and have some concept of postHandle
|
||||
if (article?.content) {
|
||||
const articleDom = parseHTML(article.content).document
|
||||
const codeBlocks = articleDom.querySelectorAll('code')
|
||||
const codeBlocks = articleDom.querySelectorAll(
|
||||
'code, pre[class^="prism-"], pre[class^="language-"]'
|
||||
)
|
||||
if (codeBlocks.length > 0) {
|
||||
codeBlocks.forEach((e) => {
|
||||
if (e.textContent) {
|
||||
|
|
@ -333,7 +339,7 @@ export const parsePreparedContent = async (
|
|||
logger.info('parse-article completed')
|
||||
|
||||
return {
|
||||
domContent: preparedDocument.document,
|
||||
domContent: document,
|
||||
parsedContent: article,
|
||||
canonicalUrl,
|
||||
pageType: parseOriginalContent(dom),
|
||||
|
|
@ -460,6 +466,8 @@ export const parseEmailAddress = (from: string): addressparser.EmailAddress => {
|
|||
export const fetchFavicon = async (
|
||||
url: string
|
||||
): Promise<string | undefined> => {
|
||||
// don't fetch favicon for fake urls
|
||||
if (url.startsWith(FAKE_URL_PREFIX)) return undefined
|
||||
try {
|
||||
// get the correct url if it's a redirect
|
||||
const response = await axios.head(url, { timeout: 5000 })
|
||||
|
|
@ -485,3 +493,31 @@ const nhm = new NodeHtmlMarkdown(
|
|||
export const htmlToMarkdown = (html: string) => {
|
||||
return nhm.translate(/* html */ html)
|
||||
}
|
||||
|
||||
export const getDistillerResult = async (
|
||||
uid: string,
|
||||
html: string
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const url = process.env.DISTILLER_URL
|
||||
if (!url) {
|
||||
console.log('No distiller url')
|
||||
return undefined
|
||||
}
|
||||
|
||||
const exp = Math.floor(Date.now() / 1000) + 60 * 60 // 1 hour
|
||||
const auth = (await signToken({ uid, exp }, env.server.jwtSecret)) as string
|
||||
|
||||
console.debug('Parsing by distiller', url)
|
||||
const response = await axios.post<string>(url, html, {
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
},
|
||||
timeout: 5000,
|
||||
})
|
||||
return response.data
|
||||
} catch (e) {
|
||||
console.log('Error parsing by distiller', e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { EveryIoHandler } from './newsletters/every-io-handler'
|
|||
import { EnergyWorldHandler } from './newsletters/energy-world'
|
||||
import { IndiaTimesHandler } from './newsletters/india-times-handler'
|
||||
|
||||
const validateUrlString = (url: string) => {
|
||||
const validateUrlString = (url: string): boolean => {
|
||||
const u = new URL(url)
|
||||
// Make sure the URL is http or https
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
||||
|
|
@ -49,6 +49,8 @@ const validateUrlString = (url: string) => {
|
|||
if (/^(10|172\.16|192\.168)\..*/.test(u.hostname)) {
|
||||
throw new Error('Invalid URL is private ip')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const contentHandlers: ContentHandler[] = [
|
||||
|
|
@ -70,6 +72,7 @@ const contentHandlers: ContentHandler[] = [
|
|||
new BloombergNewsletterHandler(),
|
||||
new SubstackHandler(),
|
||||
new StackOverflowHandler(),
|
||||
new EnergyWorldHandler(),
|
||||
]
|
||||
|
||||
const newsletterHandlers: ContentHandler[] = [
|
||||
|
|
|
|||
|
|
@ -16,4 +16,29 @@ export class EnergyWorldHandler extends ContentHandler {
|
|||
input.from === 'ETEnergyworld Latest News<newsletter@etenergyworld.com>'
|
||||
)
|
||||
}
|
||||
|
||||
shouldPreParse(url: string, dom: Document): boolean {
|
||||
return dom.querySelectorAll('img[src*="etenergyworld.png"]').length > 0
|
||||
}
|
||||
|
||||
async preParse(url: string, dom: Document): Promise<Document> {
|
||||
// get the main content
|
||||
const main = dom.querySelector('table[class="nletter-wrap"]')
|
||||
if (!main) {
|
||||
return Promise.resolve(dom)
|
||||
}
|
||||
|
||||
// create a new dom
|
||||
const newDom = dom.createDocumentFragment()
|
||||
|
||||
// add the content to the new dom
|
||||
main.querySelectorAll('table[class="multi-cols"] tr').forEach((tr) => {
|
||||
const p = dom.createElement('p')
|
||||
p.innerHTML = tr.innerHTML
|
||||
newDom.appendChild(p)
|
||||
})
|
||||
dom.body.replaceChildren(newDom)
|
||||
|
||||
return Promise.resolve(dom)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ContentHandler } from '../content-handler'
|
||||
import addressparser from 'addressparser'
|
||||
|
||||
export class IndiaTimesHandler extends ContentHandler {
|
||||
constructor() {
|
||||
|
|
@ -13,7 +14,9 @@ export class IndiaTimesHandler extends ContentHandler {
|
|||
dom: Document
|
||||
}): Promise<boolean> {
|
||||
return Promise.resolve(
|
||||
input.from === 'The Times of India <newsletters@timesofindia.com>'
|
||||
addressparser(input.from).some(
|
||||
(e) => e.address === 'newsletters@timesofindia.com'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -362,7 +362,7 @@ describe('Newsletter email test', () => {
|
|||
const html = load('./test/data/india-times-newsletter.html')
|
||||
const handler = await getNewsletterHandler({
|
||||
html,
|
||||
from: 'The Times of India <newsletters@timesofindia.com>',
|
||||
from: 'The Times of India <newsletters@timesofindia.com>',
|
||||
headers: {},
|
||||
})
|
||||
expect(handler).to.be.instanceOf(IndiaTimesHandler)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ const { encode } = require("urlsafe-base64");
|
|||
const crypto = require("crypto");
|
||||
|
||||
const Url = require('url');
|
||||
// const puppeteer = require('puppeteer-extra');
|
||||
const axios = require('axios');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { promisify } = require('util');
|
||||
|
|
@ -26,9 +25,10 @@ puppeteer.use(StealthPlugin());
|
|||
|
||||
// Add adblocker plugin to block all ads and trackers (saves bandwidth)
|
||||
const AdblockerPlugin = require('puppeteer-extra-plugin-adblocker');
|
||||
const createDOMPurify = require("dompurify");
|
||||
puppeteer.use(AdblockerPlugin({ blockTrackers: true }));
|
||||
|
||||
const createDOMPurify = require("dompurify");
|
||||
|
||||
const storage = new Storage();
|
||||
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : [];
|
||||
const previewBucket = process.env.PREVIEW_IMAGE_BUCKET ? storage.bucket(process.env.PREVIEW_IMAGE_BUCKET) : undefined;
|
||||
|
|
@ -296,7 +296,7 @@ async function fetchContent(req, res) {
|
|||
|
||||
if (contentType === 'application/pdf') {
|
||||
const uploadedFileId = await uploadPdf(finalUrl, userId, articleSavingRequestId);
|
||||
const l = await saveUploadedPdf(userId, finalUrl, uploadedFileId, articleSavingRequestId);
|
||||
await saveUploadedPdf(userId, finalUrl, uploadedFileId, articleSavingRequestId);
|
||||
} else {
|
||||
if (!content || !title) {
|
||||
const result = await retrieveHtml(page, logRecord);
|
||||
|
|
@ -409,32 +409,6 @@ function getUrl(req) {
|
|||
return parsed.href;
|
||||
}
|
||||
|
||||
async function blockResources(client) {
|
||||
const blockedResources = [
|
||||
// Assets
|
||||
// '*/favicon.ico',
|
||||
// '.css',
|
||||
// '.jpg',
|
||||
// '.jpeg',
|
||||
// '.png',
|
||||
// '.svg',
|
||||
// '.woff',
|
||||
|
||||
// Analytics and other fluff
|
||||
'*.optimizely.com',
|
||||
'everesttech.net',
|
||||
'userzoom.com',
|
||||
'doubleclick.net',
|
||||
'googleadservices.com',
|
||||
'adservice.google.com/*',
|
||||
'connect.facebook.com',
|
||||
'connect.facebook.net',
|
||||
'sp.analytics.yahoo.com',
|
||||
]
|
||||
|
||||
await client.send('Network.setBlockedURLs', { urls: blockedResources });
|
||||
}
|
||||
|
||||
async function retrievePage(url, logRecord, functionStartTime) {
|
||||
validateUrlString(url);
|
||||
|
||||
|
|
@ -494,8 +468,6 @@ async function retrievePage(url, logRecord, functionStartTime) {
|
|||
} catch {}
|
||||
});
|
||||
|
||||
await blockResources(client);
|
||||
|
||||
/*
|
||||
* Disallow MathJax from running in Puppeteer and modifying the document,
|
||||
* we shall instead run it in our frontend application to transform any
|
||||
|
|
@ -504,7 +476,8 @@ async function retrievePage(url, logRecord, functionStartTime) {
|
|||
await page.setRequestInterception(true);
|
||||
let requestCount = 0;
|
||||
page.on('request', request => {
|
||||
if (['font', 'image', 'media'].includes(request.resourceType())) {
|
||||
if (request.resourceType() === 'font') {
|
||||
// Disallow fonts from loading
|
||||
request.abort();
|
||||
return;
|
||||
}
|
||||
|
|
@ -584,7 +557,7 @@ async function retrieveHtml(page, logRecord) {
|
|||
}
|
||||
})();
|
||||
}),
|
||||
await page.waitForTimeout(5000),
|
||||
page.waitForTimeout(5000),
|
||||
]);
|
||||
logRecord.timing = { ...logRecord.timing, pageScrolled: Date.now() - pageScrollingStart };
|
||||
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ Readability.prototype = {
|
|||
|
||||
// These are the classes that readability sets itself.
|
||||
CLASSES_TO_PRESERVE: [
|
||||
"page", "twitter-tweet", "tweet-placeholder", "instagram-placeholder", "morning-brew-markets"
|
||||
"page", "twitter-tweet", "tweet-placeholder", "instagram-placeholder", "morning-brew-markets", "prism-code"
|
||||
],
|
||||
|
||||
// Classes of placeholder elements that can be empty but shouldn't be removed
|
||||
|
|
@ -545,6 +545,7 @@ Readability.prototype = {
|
|||
|
||||
const proxySrc = this.createImageProxyUrl(absoluteSrc, width, height);
|
||||
image.setAttribute('src', proxySrc);
|
||||
image.setAttribute('data-omnivore-original-src', absoluteSrc)
|
||||
}
|
||||
|
||||
// remove crossorigin attribute to avoid CORS errors
|
||||
|
|
@ -2975,8 +2976,6 @@ Readability.prototype = {
|
|||
const byline = metadata.byline || this._articleByline;
|
||||
const [author, publishedAt] = extractPublishedDateFromAuthor(byline);
|
||||
|
||||
this.log("Grabbed: " + articleContent.innerHTML);
|
||||
|
||||
this._postProcessContent(articleContent);
|
||||
|
||||
// If we haven't found an excerpt in the article's metadata, use the article's
|
||||
|
|
|
|||
52
packages/readabilityjs/test/generate_static.py
Normal file
52
packages/readabilityjs/test/generate_static.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#! /usr/bin/env python3
|
||||
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
|
||||
testdirs = [f for f in listdir(
|
||||
"test-pages") if not isfile(join("test-pages", f))]
|
||||
|
||||
file = open("index.html", "w")
|
||||
file.write("""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="script.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td valign="top" style="width: 250px">
|
||||
<ul>
|
||||
""")
|
||||
|
||||
for testdir in testdirs:
|
||||
file.write(f"""
|
||||
<li>{testdir}<br />
|
||||
<a href="./test-pages/{testdir}/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/{testdir}/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/{testdir}/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
""")
|
||||
|
||||
|
||||
file.write("""
|
||||
</ul>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<iframe name="iframe_b" frameborder="1" scrolling="yes" width="100%" height="2500">
|
||||
<p>Your browser does not support iframes.</p>
|
||||
</iframe>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
""")
|
||||
|
||||
file.close()
|
||||
472
packages/readabilityjs/test/index.html
Normal file
472
packages/readabilityjs/test/index.html
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="script.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td valign="top" style="width: 250px">
|
||||
<ul>
|
||||
|
||||
<li>danwang<br />
|
||||
<a href="./test-pages/danwang/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/danwang/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/danwang/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>omnivore_getting_started<br />
|
||||
<a href="./test-pages/omnivore_getting_started/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/omnivore_getting_started/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/omnivore_getting_started/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>nymag<br />
|
||||
<a href="./test-pages/nymag/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/nymag/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/nymag/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>computerenhance.com<br />
|
||||
<a href="./test-pages/computerenhance.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/computerenhance.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/computerenhance.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>substack-michaelshellenberger<br />
|
||||
<a href="./test-pages/substack-michaelshellenberger/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/substack-michaelshellenberger/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/substack-michaelshellenberger/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>stratechery<br />
|
||||
<a href="./test-pages/stratechery/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/stratechery/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/stratechery/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>cnbc<br />
|
||||
<a href="./test-pages/cnbc/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/cnbc/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/cnbc/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>elidourado<br />
|
||||
<a href="./test-pages/elidourado/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/elidourado/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/elidourado/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>financialpost-fishing-for-chips<br />
|
||||
<a href="./test-pages/financialpost-fishing-for-chips/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/financialpost-fishing-for-chips/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/financialpost-fishing-for-chips/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>cavesocial<br />
|
||||
<a href="./test-pages/cavesocial/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/cavesocial/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/cavesocial/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>newsletters<br />
|
||||
<a href="./test-pages/newsletters/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/newsletters/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/newsletters/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>spakhm<br />
|
||||
<a href="./test-pages/spakhm/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/spakhm/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/spakhm/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>nytimes-podcasts<br />
|
||||
<a href="./test-pages/nytimes-podcasts/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/nytimes-podcasts/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/nytimes-podcasts/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>china.substack<br />
|
||||
<a href="./test-pages/china.substack/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/china.substack/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/china.substack/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>community.musictribe.com<br />
|
||||
<a href="./test-pages/community.musictribe.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/community.musictribe.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/community.musictribe.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>berthub-2<br />
|
||||
<a href="./test-pages/berthub-2/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/berthub-2/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/berthub-2/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>samcurry<br />
|
||||
<a href="./test-pages/samcurry/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/samcurry/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/samcurry/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>variety<br />
|
||||
<a href="./test-pages/variety/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/variety/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/variety/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>infoproc<br />
|
||||
<a href="./test-pages/infoproc/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/infoproc/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/infoproc/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>computer.rip<br />
|
||||
<a href="./test-pages/computer.rip/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/computer.rip/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/computer.rip/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>news.utexas<br />
|
||||
<a href="./test-pages/news.utexas/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/news.utexas/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/news.utexas/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>electrek<br />
|
||||
<a href="./test-pages/electrek/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/electrek/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/electrek/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>sciencedirect<br />
|
||||
<a href="./test-pages/sciencedirect/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/sciencedirect/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/sciencedirect/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>jacobbrazeal<br />
|
||||
<a href="./test-pages/jacobbrazeal/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/jacobbrazeal/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/jacobbrazeal/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>getting_started_with_omnivore<br />
|
||||
<a href="./test-pages/getting_started_with_omnivore/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/getting_started_with_omnivore/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/getting_started_with_omnivore/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>berthub<br />
|
||||
<a href="./test-pages/berthub/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/berthub/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/berthub/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>josephg<br />
|
||||
<a href="./test-pages/josephg/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/josephg/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/josephg/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>gflownet<br />
|
||||
<a href="./test-pages/gflownet/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/gflownet/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/gflownet/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>gdcvault<br />
|
||||
<a href="./test-pages/gdcvault/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/gdcvault/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/gdcvault/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>instyle<br />
|
||||
<a href="./test-pages/instyle/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/instyle/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/instyle/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>slowboring<br />
|
||||
<a href="./test-pages/slowboring/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/slowboring/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/slowboring/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>bookofhook.blogspot.com<br />
|
||||
<a href="./test-pages/bookofhook.blogspot.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/bookofhook.blogspot.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/bookofhook.blogspot.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>dailymail<br />
|
||||
<a href="./test-pages/dailymail/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/dailymail/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/dailymail/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>fiercepharma<br />
|
||||
<a href="./test-pages/fiercepharma/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/fiercepharma/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/fiercepharma/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>nytimes<br />
|
||||
<a href="./test-pages/nytimes/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/nytimes/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/nytimes/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>wechat<br />
|
||||
<a href="./test-pages/wechat/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/wechat/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/wechat/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>milkroad<br />
|
||||
<a href="./test-pages/milkroad/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/milkroad/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/milkroad/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>robinwieruch.de<br />
|
||||
<a href="./test-pages/robinwieruch.de/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/robinwieruch.de/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/robinwieruch.de/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>github-blog<br />
|
||||
<a href="./test-pages/github-blog/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/github-blog/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/github-blog/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>johnhcochrane.blogspot<br />
|
||||
<a href="./test-pages/johnhcochrane.blogspot/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/johnhcochrane.blogspot/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/johnhcochrane.blogspot/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>jsomers<br />
|
||||
<a href="./test-pages/jsomers/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/jsomers/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/jsomers/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>medium<br />
|
||||
<a href="./test-pages/medium/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/medium/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/medium/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>erik-engheim<br />
|
||||
<a href="./test-pages/erik-engheim/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/erik-engheim/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/erik-engheim/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>habr.com<br />
|
||||
<a href="./test-pages/habr.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/habr.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/habr.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>fast-company<br />
|
||||
<a href="./test-pages/fast-company/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/fast-company/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/fast-company/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>youtube-embed<br />
|
||||
<a href="./test-pages/youtube-embed/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/youtube-embed/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/youtube-embed/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>ottawacitizen.com<br />
|
||||
<a href="./test-pages/ottawacitizen.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/ottawacitizen.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/ottawacitizen.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>danluu<br />
|
||||
<a href="./test-pages/danluu/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/danluu/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/danluu/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>substack-email<br />
|
||||
<a href="./test-pages/substack-email/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/substack-email/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/substack-email/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>moxie.org<br />
|
||||
<a href="./test-pages/moxie.org/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/moxie.org/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/moxie.org/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>vanityfair<br />
|
||||
<a href="./test-pages/vanityfair/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/vanityfair/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/vanityfair/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>stackoverflow<br />
|
||||
<a href="./test-pages/stackoverflow/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/stackoverflow/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/stackoverflow/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>blog.jetbrains.com<br />
|
||||
<a href="./test-pages/blog.jetbrains.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/blog.jetbrains.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/blog.jetbrains.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>bitfieldconsulting<br />
|
||||
<a href="./test-pages/bitfieldconsulting/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/bitfieldconsulting/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/bitfieldconsulting/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>techcrunch<br />
|
||||
<a href="./test-pages/techcrunch/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/techcrunch/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/techcrunch/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>rootsofprogress<br />
|
||||
<a href="./test-pages/rootsofprogress/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/rootsofprogress/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/rootsofprogress/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>channelnewsasia<br />
|
||||
<a href="./test-pages/channelnewsasia/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/channelnewsasia/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/channelnewsasia/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>jon.bo<br />
|
||||
<a href="./test-pages/jon.bo/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/jon.bo/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/jon.bo/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>aboveavalon<br />
|
||||
<a href="./test-pages/aboveavalon/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/aboveavalon/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/aboveavalon/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>city-journal<br />
|
||||
<a href="./test-pages/city-journal/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/city-journal/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/city-journal/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>energias-renovables.com<br />
|
||||
<a href="./test-pages/energias-renovables.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/energias-renovables.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/energias-renovables.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>biospace<br />
|
||||
<a href="./test-pages/biospace/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/biospace/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/biospace/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>guardian<br />
|
||||
<a href="./test-pages/guardian/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/guardian/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/guardian/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>thevaluable.dev<br />
|
||||
<a href="./test-pages/thevaluable.dev/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/thevaluable.dev/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/thevaluable.dev/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>mathoverflow<br />
|
||||
<a href="./test-pages/mathoverflow/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/mathoverflow/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/mathoverflow/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>nytimes.com<br />
|
||||
<a href="./test-pages/nytimes.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/nytimes.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/nytimes.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>people<br />
|
||||
<a href="./test-pages/people/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/people/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/people/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>garymarcus<br />
|
||||
<a href="./test-pages/garymarcus/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/garymarcus/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/garymarcus/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>ft.com<br />
|
||||
<a href="./test-pages/ft.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/ft.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/ft.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>yuyue.com<br />
|
||||
<a href="./test-pages/yuyue.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/yuyue.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/yuyue.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>debugger.medium<br />
|
||||
<a href="./test-pages/debugger.medium/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/debugger.medium/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/debugger.medium/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>channelnewsasia02<br />
|
||||
<a href="./test-pages/channelnewsasia02/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/channelnewsasia02/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/channelnewsasia02/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>brookings.edu<br />
|
||||
<a href="./test-pages/brookings.edu/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/brookings.edu/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/brookings.edu/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
<li>sydney.com<br />
|
||||
<a href="./test-pages/sydney.com/source.html" target="iframe_b">[source]</a>
|
||||
<a href="./test-pages/sydney.com/expected.html" target="iframe_b">[readability]</a>
|
||||
<a href="./test-pages/sydney.com/distiller.html" target="iframe_b">[dom-distiller]</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</td>
|
||||
<td valign="top">
|
||||
<iframe name="iframe_b" frameborder="1" scrolling="yes" width="100%" height="2500">
|
||||
<p>Your browser does not support iframes.</p>
|
||||
</iframe>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<div><p>
|
||||
In a few months, the number of people wearing an Apple Watch will surpass 100 million. While the tech press spent years infatuated with stationary smart speakers and the idea of voice-only interfaces, it was the Apple Watch and utility on the wrist that ushered in a new paradigm shift in computing. We are now seeing Apple leverage the growing number of Apple Watch wearers to build a formidable health platform. The Apple Watch is a runaway train with no company in a position to slow it down.
|
||||
</p><p>
|
||||
<strong>Mirages and Head Fakes</strong>
|
||||
</p><p>
|
||||
We are coming off of a weird stretch for the tech industry. As smartphone sales growth slowed in the mid-2010s, companies, analysts, and pundits began to search for the next big thing. The search landed on stationary smart speakers and voice interfaces.
|
||||
</p><p>
|
||||
Companies who weren’t able to leverage the smartphone revolution with their own hardware placed massive bets on digital voice assistants that would supposedly usher in the end of the smartphone era. These digital voice assistants would be delivered to consumers via cheap stationary speakers placed in the home. Massive PR campaigns were launched that attempted to convince people about this post-smartphone future. Unfortunately for these companies, glowing press coverage cannot hide a product category’s fundamental design shortcomings.
|
||||
</p><p>
|
||||
At nearly every turn, Apple was said to be missing the voice train because of a dependency on iPhone revenue. Management was said to suffer from tunnel vision while the company’s approach to privacy was positioned as a long-term headwind that would lead to inferior results in AI relative to the competition. Simply put, Apple was viewed as losing control of where technology was headed following the mobile revolution.
|
||||
</p><p>
|
||||
There were glaring signs that narratives surrounding smart speakers and Apple lacking a coherent strategy for the future were off the mark. In November 2017, I wrote the following in an article titled, “<a href="https://www.aboveavalon.com/notes/2017/11/30/a-stationary-smart-speaker-mirage">A Stationary Smart Speaker Mirage</a>”:
|
||||
</p><p>
|
||||
<em>“On the surface, Amazon Echo sales point to a burgeoning product category. A 15M+ annual sales pace for a product category that is only three years old is quite the accomplishment. This has led to prognostications of stationary smart speakers representing a new paradigm in technology. However, relying too much on Echo sales will lead to incomplete or faulty conclusions. The image portrayed by Echo sales isn't what it seems. In fact, it is only a matter of time before it becomes clear the stationary home speaker is shaping up to be one of the largest head fakes in tech. We are already starting to see early signs of disappointment begin to appear…</em>
|
||||
</p><p>
|
||||
<em>I don’t think stationary smart speakers represent the future of computing. Instead, companies are using smart speakers to take advantage of an awkward phase of technology in which there doesn’t seem to be any clear direction as to where things are headed. Consumers are buying cheap smart speakers powered by digital voice assistants without having any strong convictions regarding how such voice assistants should or can be used. The major takeaway from customer surveys regarding smart speaker usage is that there isn’t any clear trend. If anything, smart speakers are being used for rudimentary tasks that can just as easily be done with digital voice assistants found on smartwatches or smartphones. This environment paints a very different picture of the current health of the smart speaker market. The narrative in the press is simply too rosy and optimistic.</em>
|
||||
</p><p>
|
||||
<em>Ultimately, smart speakers end up competing with a seemingly unlikely product category: wearables.”</em>
|
||||
</p><p>
|
||||
Three years later, I wouldn’t change one thing found in the preceding three paragraphs. The smart speaker bubble popped less than 12 months after publishing that article. The product category no longer has a buzz factor, and despite the hopes of Amazon and Google, people are not using stationary speakers for much else besides listening to music and rudimentary tasks like setting kitchen timers.
|
||||
</p><p>
|
||||
The primary problem found with voice is that it’s not a great medium for transferring a lot of data, information, and context. As a result, companies like Amazon have needed to dial back their grandiose vision for voice-first and voice-only paradigms. <a href="https://www.aboveavalon.com/dailypremiumupdate/2020/9/28/amazons-new-products-amazons-declining-hardware-goodwill-the-battle-for-the-home">Last week’s Amazon hardware event highlighted a growing bet on screens</a> – a complete reversal from the second half of the 2010s.
|
||||
</p><p>
|
||||
<strong>Betting on the Wrist </strong>
|
||||
</p><p>
|
||||
As companies who missed the smartphone boat were placing bets on stationary speakers, Apple was placing a dramatically different bet on a small device with a screen. This device wouldn’t be stationary but instead push the definition of mobile by being worn on the wrist.
|
||||
</p><p>
|
||||
Jony Ive, who is credited with leading Apple’s push into wrist wearables, <a href="https://www.newyorker.com/magazine/2015/02/23/shape-things-come">referred to the wrist</a> as <em>“the obvious and right place”</em> for a different kind of computer.
|
||||
</p><p>
|
||||
When Apple unveiled the Apple Watch in 2014, wearable computing on the wrist was more of a promise than anything else. Apple created an entirely new industry – something that isn’t found much in the traditional Apple playbook.
|
||||
</p><p>
|
||||
After years of deep skepticism and cynicism, consensus reaction towards Apple Watch has changed and is now positive. Much of this is due to the fact that it’s impossible to miss Apple Watches appearing on wrists around the world. According to my estimates, <a href="https://www.aboveavalon.com/dailypremiumupdate/2020/9/23/thoughts-on-iphone-widgets-iphone-widgets-vs-apple-watch-apple-watch-adoption-estimates">approximately 35% of iPhone users in the U.S. now wear an Apple Watch</a>. This is a shockingly high percentage for a five-year-old product category, and it says a lot about how Apple’s intuition about the wrist was right.
|
||||
</p><p>
|
||||
<strong>Apple Watch Installed Base </strong>
|
||||
</p><p>
|
||||
The number of people wearing an Apple Watch continues to steadily increase. <a href="https://www.aboveavalon.com/dailypremiumupdate/2020/9/10/the-number-of-apple-watch-wearers-softbanks-big-bet-on-tech-stocks-gm-partners-with-nikola">According to my estimate, there were 81 million people wearing an Apple Watch as of the end of June</a>. According to Apple, 75% of Apple Watch sales are going to first-time customers. This means that 23 million people will have bought their first Apple Watch in 2020. To put that number in context, there are about 25 million people wearing a Fitbit. The Apple Watch installed base is increasing by the size of Fitbit’s overall installed base every 12 months. Exhibit 1 highlights the change in the Apple Watch installed base over the years.
|
||||
</p><p>
|
||||
<strong>Exhibit 1: Apple Watch Installed Base (number of people wearing an Apple Watch)</strong>
|
||||
</p><figure><img src="https://images.squarespace-cdn.com/content/v1/5446f93de4b0a3452dfaf5b0/1601503331287-7RZMPL0P3XT26HSYW2PM/ke17ZwdGBToddI8pDm48kHcGqbMF45vlywWRsFwnaVR7gQa3H78H3Y0txjaiv_0fDoOvxcdMmMKkDsyUqMSsMWxHk725yiiHCCLfrh8O1z5QPOohDIaIeljMHgDF5CVlOqpeNLcJ80NK65_fV7S1UaZ2-FSNrAucjbS64-gnZlYgEPKXZhIlV6X43N9eVyPBm7cT0R_dexc_UL_zbpz6JQ/Apple+Watch+Installed+Base+%28Above+Avalon%29" alt="Apple Watch Installed Base (Above Avalon)"/><figcaption>View fullsize</figcaption></figure><p>
|
||||
<em>(The calculations and methodology used to reach my Apple Watch installed base estimates is available</em> <a href="https://www.aboveavalon.com/dailypremiumupdate/2020/9/10/the-number-of-apple-watch-wearers-softbanks-big-bet-on-tech-stocks-gm-partners-with-nikola"><em>here</em></a> <em>for Above Avalon members.)</em>
|
||||
</p><p>
|
||||
<strong>Deriving Power</strong>
|
||||
</p><p>
|
||||
From where is Apple Watch deriving its momentum? The answer is found in The Grand Unified Theory of Apple Products.
|
||||
</p><figure><img src="https://images.squarespace-cdn.com/content/v1/5446f93de4b0a3452dfaf5b0/1601497689595-TLHGYI10XZ5LT7N7KFUK/ke17ZwdGBToddI8pDm48kIwk48pS_D3V01G02IYvFnV7gQa3H78H3Y0txjaiv_0fDoOvxcdMmMKkDsyUqMSsMWxHk725yiiHCCLfrh8O1z5QPOohDIaIeljMHgDF5CVlOqpeNLcJ80NK65_fV7S1URXz3wnigOGf2UwKQWypmFO3jTDBiErN2R-MkRJJMM9_OpYghpI-Ha_TwZsqqmJXng/The+Grand+Unified+Theory+of+Apple+Products+%28Above+Avalon%29" alt="The Grand Unified Theory of Apple Products (Above Avalon)"/><figcaption>View fullsize</figcaption></figure><p>
|
||||
One of the core tenets of my theory is that an Apple product category's design is tied to the role it is meant to play relative to other Apple products. The Apple Watch is designed to handle a growing number of tasks once given to the iPhone. Meanwhile, the iPhone is designed to handle a growing number of tasks given to the iPad. One can continue this exercise to cover all of Apple's major product categories.
|
||||
</p><p>
|
||||
Apple Watch is not an iPhone replacement because there are things done on an iPhone that can't be done on an Apple Watch. This ends up being a feature, not a bug. The Apple Watch’s design then allows the product to handle entirely new tasks that can’t be handled on an iPhone. This latter attribute goes a long way in explaining <a href="https://www.aboveavalon.com/notes/2020/2/5/apple-watch-and-a-paradigm-shift-in-computing">how Apple Watch has helped usher in a new paradigm shift in computing</a>. Apple Watch wearers are able to interact with technology differently.
|
||||
</p><p>
|
||||
(<em>More on The Grand Unified Theory of Apple Products is found in the Above Avalon Report, “Product Vision: How Apple Thinks About the World,” </em><a href="https://www.aboveavalon.com/reports-archive/2019/4/15/product-vision-how-apple-thinks-about-the-world" target="_blank"><em>available here</em></a><em> for Above Avalon members.)</em>
|
||||
</p><p>
|
||||
<strong>A Health Platform</strong>
|
||||
</p><p>
|
||||
In January 2019, Tim Cook surprised many by saying Apple will be remembered more for its contributions to health than for any other reason. <a href="https://www.cnbc.com/2019/01/08/tim-cook-teases-new-apple-services-tied-to-health-care.html">Here’s Cook:</a>
|
||||
</p><p>
|
||||
<em>“I believe, if you zoom out into the future, and you look back, and you ask the question, ‘What was Apple’s greatest contribution to mankind?’ it will be about health.”</em>
|
||||
</p><p>
|
||||
Many assumed that Cook’s comment hinted at Apple unveiling a portfolio of medical-grade devices that would go through the FDA approval process. Such thinking was based on a fundamental misunderstanding of Apple’s ambition and approach to product development.
|
||||
</p><p>
|
||||
Apple’s health strategy is based on leveraging hardware, software, and services to rethink the way we approach health. This means Apple wasn’t going to just launch a depository for our health data – something that is needed but which ultimately falls short of being truly revolutionary. In addition, Apple wasn’t going to just offer health and fitness services that amount to counting steps or keeping track of miles run.
|
||||
</p><p>
|
||||
By the time Cook gave his bullish comment about health, Apple had already placed its big bet on health four years earlier by unveiling the Apple Watch. In what ended up being one of Apple’s best decisions, the company avoided going the route of medical-grade devices requiring government agency approval to reach consumers. Instead, Apple framed its health platform as a new-age computer that ultimately is an iPhone alternative.
|
||||
</p><p>
|
||||
Health monitoring is one of the key new tasks that the Apple Watch, not iPhone, handles. To be more precise, Apple Watch is handling the following four health-related items:
|
||||
</p><ol><li><p>
|
||||
Proactive monitoring (i.e. heart rate and blood oxygen)
|
||||
</p></li><li><p>
|
||||
Well-being assistance (i.e. sleep monitoring including the runup to sleep)
|
||||
</p></li><li><p>
|
||||
Fitness and activity tracking (i.e. Activity and Workout apps)
|
||||
</p></li><li><p>
|
||||
Fitness and health activity (i.e. Apple Fitness+)
|
||||
</p></li></ol><p>
|
||||
With Apple Fitness+, Apple didn’t just release a virtual fitness class service. Instead, <a href="https://www.aboveavalon.com/dailypremiumupdate/2020/9/18/apples-time-flies-event-review-consolidated">Apple Fitness+ is an Apple Watch service</a>. In some ways, Apple Fitness+ reminds me of Apple TV+. A future in which Fitness+ workouts are available on third-party gym equipment displays including on treadmills and stationary bikes is not a stretch. In addition, <a href="https://www.aboveavalon.com/dailypremiumupdate/2020/9/24/nike-earnings-the-similarity-between-nike-and-apple-a-stronger-apple-and-nike-partnership">classes from other companies such as Nike could further elevate Apple Fitness</a>+.
|
||||
</p><p>
|
||||
<strong>Competition</strong>
|
||||
</p><p>
|
||||
If the Apple Watch is a runaway train, there is no obvious candidate in a position to stop or even slow the train. While other companies are slowly waking up and seeing the momentum found with Apple Watch, there is still much indifference, mystery, and misunderstanding as to why people are buying wearables. Too many companies still think of wearables as glorified smartphone accessories. Such thinking makes it impossible for competitors to see how Apple Watch is ushering in a paradigm shift in computing by making technology more personal in a way that other devices have failed to accomplish or replicate.
|
||||
</p><p>
|
||||
One of the main takeaways from Apple’s product event earlier this month is how Apple is its own toughest competitor. The Apple Watch’s most legitimate competition is found with older Apple Watches and non-consumption (i.e. empty wrists). While this introduces its own set of risks and challenges, there is still no genuine Apple Watch competition from other companies after six years. This is an indication of the power found in controlling your own hardware, software, and services in order to get more out of technology without having technology take over people’s lives.
|
||||
</p><p>
|
||||
<em>Listen to the corresponding Above Avalon podcast episode for this article</em> <a href="https://www.aboveavalon.com/podcast/2020/10/3/above-avalon-episode-174-apple-watch-is-a-runaway-train"><em>here</em></a><em>.</em>
|
||||
</p><p>
|
||||
<em>Receive my analysis and perspective on Apple throughout the week via exclusive daily updates (2-3 stories per day, 10-12 stories per week). Available to Above Avalon members in both written and audio forms. To sign up and for more information on membership, visit the </em><a href="http://www.aboveavalon.com/membership/" target="_blank"><em>membership page</em></a><em>.</em>
|
||||
</p><p>
|
||||
<em>For additional discussion on this topic, check out the</em> <a href="https://www.aboveavalon.com/dailypremiumupdate/2020/10/1/apple-watch-is-a-runaway-train-googles-hardware-event-siri-vs-apple-watch"><em>Above Avalon daily update from October 1st.</em></a>
|
||||
</p></div>
|
||||
310
packages/readabilityjs/test/test-pages/berthub-2/distiller.html
Normal file
310
packages/readabilityjs/test/test-pages/berthub-2/distiller.html
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
<div><p>
|
||||
All BNT162b2 vaccine data on this page is sourced from this <a href="https://mednet-communities.net/inn/db/media/docs/11889.doc" target="_blank">World Health Organization document</a>.
|
||||
</p><blockquote><p>
|
||||
This is a living page, shared already so people can get going! But check back frequently for updates.
|
||||
</p></blockquote><p>
|
||||
In short: the vaccine mRNA has been optimized by the manufacturer by changing bits of RNA from (say) <code>UUU</code> to <code>UUC</code>, and people would like to understand the logic behind these changes. This challenge is quite close to what cryptologists and reverse engineering people encounter regularly. On this page, you’ll find all the details you need to get cracking to reverse engineer just HOW the vaccine has been optimized.
|
||||
</p><p>
|
||||
I thought this would just be a fun puzzle, but I have just been informed that figuring out the optimization procedure & documenting it is tremendously important for researchers around the world, as this would help them design code for proteins and vaccines.
|
||||
</p><p>
|
||||
So, if you want to help vaccine research, do read on!
|
||||
</p><h2>
|
||||
The leader board
|
||||
</h2><p>
|
||||
Here are the current best entrants to the optimization algorithm (average of 20 runs):
|
||||
</p><table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Codon Match
|
||||
</th>
|
||||
<th>
|
||||
Nucleotide Match
|
||||
</th>
|
||||
<th>
|
||||
Author
|
||||
</th>
|
||||
<th>
|
||||
Comment
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
codon mapping
|
||||
</td>
|
||||
<td>
|
||||
79.51 %
|
||||
</td>
|
||||
<td>
|
||||
91.52 %
|
||||
</td>
|
||||
<td>
|
||||
Harry Harpel
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://github.com/berthubert/bnt162b2/blob/master/vaccine_dict.json">A simple static codon mapping</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://gist.github.com/sanxiyn/fddd1f18074076fb47e04733e6b62865">most-frequent.py</a>
|
||||
</td>
|
||||
<td>
|
||||
78.57 %
|
||||
</td>
|
||||
<td>
|
||||
91.08 %
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://twitter.com/sanxiyn">Seo Sanghyeon</a>
|
||||
</td>
|
||||
<td>
|
||||
Codon frequency optimization using python_codon_tables
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://github.com/cibo6/bnt162b2">dnachisel</a>
|
||||
</td>
|
||||
<td>
|
||||
76.99 %
|
||||
</td>
|
||||
<td>
|
||||
91.06 %
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.linkedin.com/in/erik-brauer/">Erik Brauer</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://edinburgh-genome-foundry.github.io/DnaChisel/">DNAChisel algorithm</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://gist.github.com/naomiajacobs/1e9de466ead8f362394cdfd581ec74fd#gistcomment-3578742">dnachisel</a>
|
||||
</td>
|
||||
<td>
|
||||
76.89 %
|
||||
</td>
|
||||
<td>
|
||||
90.89 %
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://twitter.com/pvieito">Pedro José Pereira Vieito</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://edinburgh-genome-foundry.github.io/DnaChisel/">DNAChisel algorithm</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://github.com/hyc/bnt162b2/commit/b7b84a114748940de724992d6a6a5fc65b454fb0">remap</a>
|
||||
</td>
|
||||
<td>
|
||||
71.11 %
|
||||
</td>
|
||||
<td>
|
||||
88.59 %
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://twitter.com/hyc_symas">Howard Chu</a>
|
||||
</td>
|
||||
<td>
|
||||
Map every codon to an amino acid, pick the best codon for that amino acid
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://github.com/unrelatedlabs/bnt162b2/blob/master/reverse.ipynb">3rd-cg.py</a>
|
||||
</td>
|
||||
<td>
|
||||
60.83 %
|
||||
</td>
|
||||
<td>
|
||||
85.11 %
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://twitter.com/pkuhar">Peter Kuhar</a>
|
||||
</td>
|
||||
<td>
|
||||
If third position is already 'G' or 'C', no change. Otherwise replace third position by a C, if protein still matches, done. Otherwise try a G.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://github.com/berthubert/bnt162b2/blob/master/3rd-gc.go">3rd-gc.go</a>
|
||||
</td>
|
||||
<td>
|
||||
53.06 %
|
||||
</td>
|
||||
<td>
|
||||
81.55 %
|
||||
</td>
|
||||
<td>
|
||||
bert hubert
|
||||
</td>
|
||||
<td>
|
||||
If third position is already 'G' or 'C', no change. Otherwise replace third position by a G, if protein still matches, done. Otherwise try a C.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://gist.github.com/naomiajacobs/1e9de466ead8f362394cdfd581ec74fd">dnachisel</a>
|
||||
</td>
|
||||
<td>
|
||||
46.33 %
|
||||
</td>
|
||||
<td>
|
||||
79.48 %
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://twitter.com/naomicodes">Naomi Jacobs</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://edinburgh-genome-foundry.github.io/DnaChisel/">DNAChisel algorithm</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
NOP
|
||||
</td>
|
||||
<td>
|
||||
27.63 %
|
||||
</td>
|
||||
<td>
|
||||
72.23 %
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
Does not do any optimization at all
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table><h2>
|
||||
BioNTech
|
||||
</h2><p>
|
||||
We should all be very grateful that BioNTech has shared this data with us. And of course we should also be grateful to the many many researchers and lab workers that worked for decades to bring the state of the art to the point that such a vaccine could be developed. It is marvelous.
|
||||
</p><p>
|
||||
Because it is so marvelous, I want to understand everything about the vaccine. I wrote a page <a href="https://berthub.eu/articles/posts/reverse-engineering-source-code-of-the-biontech-pfizer-vaccine/" target="_blank">Reverse Engineering the source code of the BioNTech/Pfizer SARS-CoV-2 Vaccine</a> that describes in some detail what is in the mRNA of the vaccine. It helps to read this page before continuing, I promise you it will be interesting.
|
||||
</p><p>
|
||||
The post left open some questions however, and this is where it gets fascinating.
|
||||
</p><h2>
|
||||
The codon optimization
|
||||
</h2><p>
|
||||
The vaccine contains RNA code for a very <em>slightly</em> modified copy of the SARS-CoV-2 S protein.
|
||||
</p><p>
|
||||
The RNA code of the vaccine itself however is <em>highly</em> modified from the viral original! This has been done by the manufacturer, based on their understanding of nature.
|
||||
</p><p>
|
||||
And from what we understand, these modifications make the vaccine <strong>much much more</strong> effective. It would be a lot of fun to understand these modifications. It might for example explain why the Moderna vaccine needs 100 micrograms and the BioNTech vaccine only 30 micrograms.
|
||||
</p><p>
|
||||
Here is the beginning of the S protein in both the virus and the BNT162b2 vaccine RNA code. Exclamation marks denote differences.
|
||||
</p><pre><code>Virus: AUG UUU GUU UUU CUU GUU UUA UUG CCA CUA GUC UCU AGU CAG UGU GUU
|
||||
Vaccine: AUG UUC GUG UUC CUG GUG CUG CUG CCU CUG GUG UCC AGC CAG UGU GUG
|
||||
! ! ! ! ! ! ! ! ! ! ! ! !
|
||||
</code></pre><p>
|
||||
RNA is a string (literally) of RNA characters, <code>A</code>, <code>C</code>, <code>G</code> and <code>U</code>. There is no physical framing on there, but it makes sense to analyse it in groups of three.
|
||||
</p><p>
|
||||
Each group (called a codon) maps to an amino acid (denoted by a capital letter). A string of amino acids is a protein. Here is what that looks like:
|
||||
</p><pre><code>Virus: AUG UUU GUU UUU CUU GUU UUA UUG CCA CUA GUC UCU AGU CAG UGU GUU
|
||||
M F V F L V L L P L V S S Q C V
|
||||
Vaccine: AUG UUC GUG UUC CUG GUG CUG CUG CCU CUG GUG UCC AGC CAG UGU GUG
|
||||
! ! ! ! ! ! ! ! ! ! ! ! !
|
||||
</code></pre><p>
|
||||
Here we can see that while the codons are different, the amino acid version is the same. There are 4*4*4 codons but only 20 amino acids. This means you can typically change every codon into one of two others, and still code for the same amino acid.
|
||||
</p><p>
|
||||
So in the second codon, <code>UUU</code> was changed to <code>UUC</code>. This is a net addition of one ‘C’ to the vaccine. The third codon changed from <code>GUU</code> to <code>GUG</code>, which is a net addition of one <code>G</code>.
|
||||
</p><p>
|
||||
<strong>It is known that a higher fraction of <code>G</code> and <code>C</code> characters improves the efficiency of an mRNA vaccine</strong>.
|
||||
</p><p>
|
||||
Now, if that was all there was to it, this could be the end of this page. “The algorithm is change codons so we get more G and C in there”. But then we meet the 9th codon which changes <code>CCA</code> to <code>CCU</code>.
|
||||
</p><p>
|
||||
Throughout the ~4000 characters of the vaccine, this happens many times.
|
||||
</p><h2>
|
||||
Our challenge
|
||||
</h2><p>
|
||||
The goal is: find an algorithm that modifies the ‘wild type’ RNA code into the BNT162b2 one. Because everyone would like to understand how to turn viral RNA into an effective vaccine. The algorithm does not need to reproduce the <em>exact</em> RNA code of course, but it would be super nice if it came up with something very similar, while also being brief.
|
||||
</p><p>
|
||||
To help you, I have provided the data in a number of forms, as described on <a href="https://github.com/berthubert/bnt162b2" target="_blank">the GitHub page</a>.
|
||||
</p><blockquote><p>
|
||||
Note that in these files the <code>U</code> mentioned above appears as a <code>T</code>. <code>U</code> and <code>T</code> are the RNA and DNA manifestations of the same information.
|
||||
</p></blockquote><p>
|
||||
The easiest place to start might be the ‘<a href="https://github.com/berthubert/bnt162b2/blob/master/side-by-side.csv" target="_blank">side-by-side.csv</a>‘ file. This lists the original and modified version of each codon, side by side:
|
||||
</p><pre><code>abspos,codonOrig,codonVaccine
|
||||
0,ATG,ATG
|
||||
3,TTT,TTC
|
||||
6,GTT,GTG
|
||||
...
|
||||
3813,TAC,TAC
|
||||
3816,ACA,ACA
|
||||
3819,TAA,TGA
|
||||
</code></pre><p>
|
||||
There is also an equivalency table that shows wich codons can be interchanged without changing the amino acid output. Please find this in <a href="https://github.com/berthubert/bnt162b2/blob/master/codon-table-grouped.csv" target="_blank">codon-table-grouped.csv</a>. There is also a visual version <a href="https://en.wikipedia.org/wiki/DNA_and_RNA_codon_tables#Standard_DNA_codon_table" target="_blank">here</a>.
|
||||
</p><h2>
|
||||
A sample algorithm
|
||||
</h2><p>
|
||||
On the <a href="https://github.com/berthubert/bnt162b2" target="_blank">GitHub repository</a> you can find <a href="https://github.com/berthubert/bnt162b2/blob/master/3rd-gc.go" target="_blank">3rd-gc.go</a> (and <a href="https://github.com/berthubert/bnt162b2/blob/master/3rd-gc.py" target="_blank">3rd-gc.py</a>).
|
||||
</p><p>
|
||||
These implement a simple strategy that works like this:
|
||||
</p><ul><li>If a virus codon already ended on G or C, copy it to the vaccine mRNA</li><li>If not, replace last nucleotide in codon by a G, see if the amino acid still matches, if so, copy to the vaccine mRNA</li><li>Try the same with a C</li><li>Otherwise copy as is</li></ul><p>
|
||||
Or in <code>golang</code>:
|
||||
</p><pre><code>// base case, don't do anything
|
||||
our = vir
|
||||
|
||||
// don't do anything if codon ends on G or C already
|
||||
if(vir[2] == 'G' || vir[2] =='C') {
|
||||
fmt.Printf("Codon ended on G or C already, not doing anything.")
|
||||
} else {
|
||||
prop = vir[:2]+"G"
|
||||
fmt.Printf("Attempting G substitution, new candidate '%s'. ", prop)
|
||||
if(c2s[vir] == c2s[prop]) {
|
||||
fmt.Printf("Amino acid still the same, done!")
|
||||
our = prop
|
||||
} else {
|
||||
fmt.Printf("Oops, amino acid changed. Trying C, new candidate '%s'. ", prop)
|
||||
prop = vir[:2]+"C"
|
||||
if(c2s[vir] == c2s[prop]) {
|
||||
fmt.Printf("Amino acid still the same, done!")
|
||||
our=prop
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
</code></pre><p>
|
||||
This achieves a rather poor 53.1% match with the BioNTech RNA vaccine, but it is a start.
|
||||
</p><p>
|
||||
When you design your algorithm, be sure to only base your choices on the virus RNA. Do not peek into the BioNTech RNA!
|
||||
</p><p>
|
||||
If you have achieved a score beyond 53.1% please email a link to your code to bert@hubertnet.nl (or <a href="https://twitter.com/PowerDNS_Bert" target="_blank">@PowerDNS_Bert</a> and I’ll put it on the leader board at the top of this page!
|
||||
</p><h2>
|
||||
Things that will help
|
||||
</h2><p>
|
||||
As with every form of reverse engineering or cryptanalysis, it helps to understand what we are looking at.
|
||||
</p><h2>
|
||||
GC ratio
|
||||
</h2><p>
|
||||
We know that one goal of the ‘codon optimization’ is to get more <code>C</code>s and <code>G</code>s into the vaccine version of the RNA. However, there is also a limit to that. In DNA, which is also used to manufacture the vaccine, <code>G</code> and <code>C</code> bind together strongly, to the point that if you put too many of these ‘nucleotides’ in there, the DNA will no longer be replicated efficiently.
|
||||
</p><p>
|
||||
So some modifications may actually happen to manage <em>down</em> the GC percentage of a stretch of DNA if it was getting too high.
|
||||
</p><h2>
|
||||
Codon optimization
|
||||
</h2><p>
|
||||
Some codons are rare in human DNA, or in certain cells. It may be that some codons are replaced by other ones simply because they are more frequently used by some cells.
|
||||
</p><h2>
|
||||
RNA folding
|
||||
</h2><p>
|
||||
We’ve been looking at codons up to here. The RNA itself however does not know about codons, there are no markers that say where a codon begins and ends. The first codon on a protein however is always ATG (or AUG in RNA).
|
||||
</p><p>
|
||||
RNA curls up into a shape. This shape might help evade the immune system or it might improve translation into amino acids. This only depends on the sequence of RNA nucleotides and not on specific codons.
|
||||
</p><p>
|
||||
You can submit RNA sequences to <a href="http://rna.tbi.univie.ac.at/cgi-bin/RNAWebSuite/RNAfold.cgi" target="_blank">this server of the Institute for Theoretical Chemistry at the University of Vienna</a> and it will fold RNA for you. This is a very advanced server that does meticulous calculations.
|
||||
</p><p>
|
||||
This <a href="https://en.wikipedia.org/wiki/Nucleic_acid_structure_prediction" target="_blank">Wikipedia page</a> describes how this works.
|
||||
</p><p>
|
||||
It may be that some optimizations improve folding.
|
||||
</p><p>
|
||||
I am also told that this paper by Moderna (another mRNA vaccine manufacturer) may be relevant: <a href="https://www.pnas.org/content/116/48/24075" target="_blank">mRNA structure regulates protein expression through changes in functional half-life</a>.
|
||||
</p></div>
|
||||
241
packages/readabilityjs/test/test-pages/berthub/distiller.html
Normal file
241
packages/readabilityjs/test/test-pages/berthub/distiller.html
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<div><p>
|
||||
Welcome! In this post, we’ll be taking a character-by-character look at the source code of the BioNTech/Pfizer SARS-CoV-2 mRNA vaccine.
|
||||
</p><blockquote><p>
|
||||
Update: The other up and coming vaccines are described in <a href="https://berthub.eu/articles/posts/genetic-code-of-covid-19-vaccines/" target="_blank">The Genetic Code and Proteins of the Other Covid-19 Vaccines</a>
|
||||
</p><p>
|
||||
<em>I want to thank the large cast of people who spent time previewing this article for legibility and correctness. All mistakes remain mine though, but I would love to hear about them quickly at bert@hubertnet.nl or <a href="https://twitter.com/PowerDNS_Bert" target="_blank">@PowerDNS_Bert</a></em>
|
||||
</p></blockquote><p>
|
||||
Now, these words may be somewhat jarring - the vaccine is a liquid that gets injected in your arm. How can we talk about source code?
|
||||
</p><p>
|
||||
This is a good question, so let’s start off with a small part of the very source code of the BioNTech/Pfizer vaccine, also known as <a href="https://en.wikipedia.org/wiki/Tozinameran" target="_blank">BNT162b2</a>, also known as Tozinameran <a href="https://twitter.com/PowerDNS_Bert/status/1342109138965422083" target="_blank">also known as Comirnaty</a>.
|
||||
</p><figure><img src="/articles/bnt162b2.png" alt="First 500 characters of the BNT162b2 mRNA. Source: World Health Organization"/><figcaption>
|
||||
<p>
|
||||
First 500 characters of the BNT162b2 mRNA. Source: <a href="https://mednet-communities.net/inn/db/media/docs/11889.doc" target="_blank">World Health Organization</a>
|
||||
</p>
|
||||
</figcaption></figure><p>
|
||||
The BNT162b2 mRNA vaccine has this digital code at its heart. It is 4284 characters long, so it would fit in a bunch of tweets. At the very beginning of the vaccine production process, someone uploaded this code to a DNA printer (yes), which then converted the bytes on disk to actual DNA molecules.
|
||||
</p><figure><img src="/articles/bioxp-3200.jpg" alt="A Codex DNA BioXp 3200 DNA printer"/><figcaption>
|
||||
<p>
|
||||
A <a href="https://codexdna.com/products/bioxp-system/" target="_blank">Codex DNA</a> BioXp 3200 DNA printer
|
||||
</p>
|
||||
</figcaption></figure><p>
|
||||
Out of such a machine come tiny amounts of DNA, which after a lot of biological and chemical processing end up as RNA (more about which later) in the vaccine vial. A 30 microgram dose turns out to actually contain 30 micrograms of RNA. In addition, there is a clever lipid (fatty) packaging system that gets the mRNA into our cells.
|
||||
</p><blockquote><p>
|
||||
Update: Derek Lowe of the famous <a href="https://blogs.sciencemag.org/pipeline/" target="_blank">In the pipeline blog</a> over at Science has written a comprehensive post “<a href="https://blogs.sciencemag.org/pipeline/archives/2021/01/11/rna-vaccines-and-their-lipids" target="_blank">RNA Vaccines And Their Lipids</a>“ which neatly explains the lipid and delivery parts of the vaccines that I am not competent to describe. Luckily Derek is!
|
||||
</p><p>
|
||||
Update 2: Jonas Neubert and Cornelia Scheitz have written <a href="https://blog.jonasneubert.com/2021/01/10/exploring-the-supply-chain-of-the-pfizer-biontech-and-moderna-covid-19-vaccines/" target="_blank">this awesome page</a> with loads of detail on how the vaccines actually get produced and distributed. Recommended!
|
||||
</p></blockquote><p>
|
||||
RNA is the volatile ‘working memory’ version of DNA. DNA is like the flash drive storage of biology. DNA is very durable, internally redundant and very reliable. But much like computers do not execute code directly from a flash drive, before something happens, code gets copied to a faster, more versatile yet far more fragile system.
|
||||
</p><p>
|
||||
For computers, this is RAM, for biology it is RNA. The resemblance is striking. Unlike flash memory, RAM degrades very quickly unless lovingly tended to. The reason the Pfizer/BioNTech mRNA vaccine must be stored in the deepest of deep freezers is the same: RNA is a fragile flower.
|
||||
</p><p>
|
||||
Each RNA character weighs on the order of 0.53·10⁻²¹ grams, meaning there are around 6·10¹⁶ characters in a single 30 microgram vaccine dose. Expressed in bytes, this is around 14 petabytes, although it must be said this consists of around <a href="https://docs.google.com/spreadsheets/d/1vc6p9IXQVRpVQntcI1tCdSMLNDuT8fl8rags0gDxMZA/edit?usp=sharing" target="_blank">13,000 billion repetitions</a> of the same 4284 characters. The actual informational content of the vaccine is just over a kilobyte. <a href="https://www.ncbi.nlm.nih.gov/projects/sviewer/?id=NC_045512&tracks=[key:sequence_track,name:Sequence,display_name:Sequence,id:STD649220238,annots:Sequence,ShowLabel:false,ColorGaps:false,shown:true,order:1][key:gene_model_track,name:Genes,display_name:Genes,id:STD3194982005,annots:Unnamed,Options:ShowAllButGenes,CDSProductFeats:true,NtRuler:true,AaRuler:true,HighlightMode:2,ShowLabel:true,shown:true,order:9]&v=1:29903&c=null&select=null&slim=0" target="_blank">SARS-CoV-2 itself</a> weighs in at around 7.5 kilobytes.
|
||||
</p><blockquote><p>
|
||||
Update: In the original post these numbers were off. <a href="https://docs.google.com/spreadsheets/d/1vc6p9IXQVRpVQntcI1tCdSMLNDuT8fl8rags0gDxMZA/edit?usp=sharing" target="_blank">Here is a spreadsheet</a> with the correct calculations.
|
||||
</p></blockquote><h2>
|
||||
The briefest bit of background
|
||||
</h2><p>
|
||||
DNA is a digital code. Unlike computers, which use 0 and 1, life uses A, C, G and U/T (the ‘nucleotides’, ‘nucleosides’ or ‘bases’).
|
||||
</p><p>
|
||||
In computers we store the 0 and 1 as the presence or absence of a charge, or as a current, as a magnetic transition, or as a voltage, or as a modulation of a signal, or as a change in reflectivity. Or in short, the 0 and 1 are not some kind of abstract concept - they live as electrons and in many other physical embodiments.
|
||||
</p><p>
|
||||
In nature, A, C, G and U/T are molecules, stored as chains in DNA (or RNA).
|
||||
</p><p>
|
||||
In computers, we group 8 bits into a byte, and the byte is the typical unit of data being processed.
|
||||
</p><p>
|
||||
Nature groups 3 nucleotides into a codon, and this codon is the typical unit of processing. A codon contains 6 bits of information (2 bits per DNA character, 3 characters = 6 bits. This means 2⁶ = 64 different codon values).
|
||||
</p><p>
|
||||
Pretty digital so far. When in doubt, <a href="https://mednet-communities.net/inn/db/media/docs/11889.doc" target="_blank">head to the WHO document</a> with the digital code to see for yourself.
|
||||
</p><blockquote><p>
|
||||
<em>Some further reading is <a href="https://berthub.eu/articles/posts/what-is-life/" target="_blank">available here</a> - this link (‘What is life’) might help make sense of the rest of this page. Or, if you like video, I have <a href="https://berthub.eu/dna" target="_blank">two hours for you</a>.</em>
|
||||
</p></blockquote><h2>
|
||||
So what does that code DO?
|
||||
</h2><p>
|
||||
The idea of a vaccine is to teach our immune system how to fight a pathogen, without us actually getting ill. Historically this has been done by injecting a weakened or incapacitated (attenuated) virus, plus an ‘adjuvant’ to scare our immune system into action. This was a decidedly analogue technique involving billions of eggs (or insects). It also required a lot of luck and loads of time. Sometimes a different (unrelated) virus was also used.
|
||||
</p><p>
|
||||
An mRNA vaccine achieves the same thing (‘educate our immune system’) but in a laser like way. And I mean this in both senses - very narrow but also very powerful.
|
||||
</p><p>
|
||||
So here is how it works. The injection contains volatile genetic material that describes the famous SARS-CoV-2 ‘Spike’ protein. Through clever chemical means, the vaccine manages to get this genetic material into some of our cells.
|
||||
</p><p>
|
||||
These then dutifully start producing SARS-CoV-2 Spike proteins in large enough quantities that our immune system springs into action. Confronted with Spike proteins, and (importantly) tell-tale signs that cells have been taken over, our immune system develops a powerful response against multiple aspects of the Spike protein AND the production process.
|
||||
</p><p>
|
||||
And this is what gets us to the 95% efficient vaccine.
|
||||
</p><p>
|
||||
This is a sort of table of contents. We’ll start with the ‘cap’, actually depicted as a little hat.
|
||||
</p><p>
|
||||
Much like you can’t just plonk opcodes in a file on a computer and run it, the biological operating system requires headers, has linkers and things like calling conventions.
|
||||
</p><p>
|
||||
The code of the vaccine starts with the following two nucleotides:
|
||||
</p><pre><code>GA
|
||||
</code></pre><p>
|
||||
This can be compared very much to every <a href="https://en.wikipedia.org/wiki/DOS_MZ_executable" target="_blank">DOS and Windows executable starting with MZ</a>, or UNIX scripts starting with <a href="https://en.wikipedia.org/wiki/Shebang_(Unix)" target="_blank"><code>#!</code></a>. In both life and operating systems, these two characters are not executed in any way. But they have to be there because otherwise nothing happens.
|
||||
</p><p>
|
||||
The mRNA ‘cap’ <a href="https://en.wikipedia.org/wiki/Five-prime_cap#Function" target="_blank">has a number of functions</a>. For one, it marks code as coming from the nucleus. In our case of course it doesn’t, our code comes from a vaccination. But we don’t need to tell the cell that. The cap makes our code look legit, which protects it from destruction.
|
||||
</p><p>
|
||||
The initial two <code>GA</code> nucleotides are also chemically slightly different from the rest of the RNA. In this sense, the <code>GA</code> has some out-of-band signaling on it.
|
||||
</p><h2>
|
||||
The “five-prime untranslated region”
|
||||
</h2><p>
|
||||
Some lingo here. RNA molecules can only be read in one direction. Confusingly, the part where the reading begins is called the 5’ or ‘five-prime’. The reading stops at the 3’ or three-prime end.
|
||||
</p><p>
|
||||
Life consists of proteins (or things made by proteins). And these proteins are described in RNA. When RNA gets converted into proteins, this is called translation.
|
||||
</p><p>
|
||||
Here we have the 5’ untranslated region (‘UTR’), so this bit does not end up in the protein:
|
||||
</p><pre><code>GAAΨAAACΨAGΨAΨΨCΨΨCΨGGΨCCCCACAGACΨCAGAGAGAACCCGCCACC
|
||||
</code></pre><p>
|
||||
Here we encounter our first surprise. The normal RNA characters are A, C, G and U. U is also known as ’T’ in DNA. But here we find a Ψ, what is going on?
|
||||
</p><p>
|
||||
This is one of the exceptionally clever bits about the vaccine. Our body runs a powerful antivirus system (“the original one”). For this reason, cells are extremely unenthusiastic about foreign RNA and try very hard to destroy it before it does anything.
|
||||
</p><p>
|
||||
This is somewhat of a problem for our vaccine - it needs to sneak past our immune system. Over many years of experimentation, it was found that if the U in RNA is replaced by a slightly modified molecule, our immune system loses interest. For real.
|
||||
</p><p>
|
||||
So in the BioNTech/Pfizer vaccine, every U has been replaced by 1-methyl-3’-pseudouridylyl, denoted by Ψ. The really clever bit is that although this replacement Ψ placates (calms) our immune system, it is accepted as a normal U by relevant parts of the cell.
|
||||
</p><p>
|
||||
In computer security we also know this trick - it sometimes is possible to transmit a slightly corrupted version of a message that confuses firewalls and security solutions, but that is still accepted by the backend servers - which can then get hacked.
|
||||
</p><p>
|
||||
We are now reaping the benefits of fundamental scientific research performed in the past. The <a href="https://twitter.com/PennMedicine/status/1341766354232365059" target="_blank">discoverers</a> of this Ψ technique had to fight to get <a href="https://www.statnews.com/2020/11/10/the-story-of-mrna-how-a-once-dismissed-idea-became-a-leading-technology-in-the-covid-vaccine-race/" target="_blank">their</a> work funded and then accepted. We should all be very grateful, and I am sure the <a href="https://twitter.com/PowerDNS_Bert/status/1329861047168225281" target="_blank">Nobel prizes will arrive in due course</a>.
|
||||
</p><blockquote><p>
|
||||
Many people have asked, could viruses also use the Ψ technique to beat our immune systems? In short, this is extremely unlikely. Life simply does not have the machinery to build 1-methyl-3’-pseudouridylyl nucleotides. Viruses rely on the machinery of life to reproduce themselves, and this facility is simply not there. The mRNA vaccines quickly degrade in the human body, and there is no possibility of the Ψ-modified RNA replicating with the Ψ still in there. “<a href="https://www.deplatformdisease.com/blog/no-really-mrna-vaccines-are-not-going-to-affect-your-dna" target="_blank">No, Really, mRNA Vaccines Are Not Going To Affect Your DNA</a>“ is also a good read.
|
||||
</p></blockquote><p>
|
||||
Ok, back to the 5’ UTR. What do these 51 characters do? As everything in nature, almost nothing has one clear function.
|
||||
</p><p>
|
||||
When our cells need to <em>translate</em> RNA into proteins, this is done using a machine called the ribosome. The ribosome is like a 3D printer for proteins. It ingests a strand of RNA and based on that it emits a string of amino acids, which then fold into a protein.
|
||||
</p><video controls="" loop=""><source src="/articles/protein-short.mp4" type="video/mp4"/></video><p>
|
||||
This is what we see happening above. The black ribbon at the bottom is RNA. The ribbon appearing in the green bit is the protein being formed. The things flying in and out are amino acids plus adaptors to make them fit on RNA.
|
||||
</p><p>
|
||||
This ribosome needs to physically sit on the RNA strand for it to get to work. Once seated, it can start forming proteins based on further RNA it ingests. From this, you can imagine that it can’t yet read the parts where it lands on first. This is just one of the functions of the UTR: the ribosome landing zone. The UTR provides ‘lead-in’.
|
||||
</p><p>
|
||||
In addition to this, the UTR also contains metadata: when should translation happen? And how much? For the vaccine, they took the most ‘right now’ UTR they could find, taken from the <a href="https://www.tandfonline.com/doi/full/10.1080/15476286.2018.1450054" target="_blank">alpha globin gene</a>. This gene is known to robustly produce a lot of proteins. In previous years, scientists had already found ways to optimize this UTR even further (according to the WHO document), so this is not quite the alpha globin UTR. It is better.
|
||||
</p><h2>
|
||||
The S glycoprotein signal peptide
|
||||
</h2><p>
|
||||
As noted, the goal of the vaccine is to get the cell to produce copious amounts of the Spike protein of SARS-CoV-2. Up to this point, we have mostly encountered metadata and “calling convention” stuff in the vaccine source code. But now we enter the actual viral protein territory.
|
||||
</p><p>
|
||||
We still have one layer of metadata to go however. Once the ribosome (from the splendid animation above) has made a protein, that protein still needs to go somewhere. This is encoded in the “S glycoprotein signal peptide (extended leader sequence)“.
|
||||
</p><p>
|
||||
The way to see this is that at the beginning of the protein there is a sort of address label - encoded as part of the protein itself. In this specific case, the signal peptide says that this protein should exit the cell via the “endoplasmic reticulum”. Even Star Trek lingo is not as fancy as this!
|
||||
</p><p>
|
||||
The “signal peptide” is not very long, but when we look at the code, there are differences between the viral and vaccine RNA:
|
||||
</p><p>
|
||||
(Note that for comparison purposes, I have replaced the fancy modified Ψ by a regular RNA U)
|
||||
</p><pre><code> 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
|
||||
Virus: AUG UUU GUU UUU CUU GUU UUA UUG CCA CUA GUC UCU AGU CAG UGU GUU
|
||||
Vaccine: AUG UUC GUG UUC CUG GUG CUG CUG CCU CUG GUG UCC AGC CAG UGU GUG
|
||||
! ! ! ! ! ! ! ! ! ! ! ! ! !
|
||||
</code></pre><p>
|
||||
So what is going on? I have not accidentally listed the RNA in groups of 3 letters. Three RNA characters make up a codon. And every codon encodes for a specific amino acid. The signal peptide in the vaccine consists of <em>exactly</em> the same amino acids as in the virus itself.
|
||||
</p><p>
|
||||
So how come the RNA is different?
|
||||
</p><p>
|
||||
There are 4³=64 different codons, since there are 4 RNA characters, and there are three of them in a codon. Yet there are only 20 different amino acids. This means that multiple codons encode for the same amino acid.
|
||||
</p><p>
|
||||
Life uses the following nearly universal table for mapping RNA codons to amino acids:
|
||||
</p><figure><img src="/articles/rna-codon-table.png" alt="The RNA codon table (Wikipedia)"/><figcaption>
|
||||
<p>
|
||||
<a href="https://en.wikipedia.org/wiki/DNA_and_RNA_codon_tables" target="_blank">The RNA codon table</a> (Wikipedia)
|
||||
</p>
|
||||
</figcaption></figure><p>
|
||||
In this table, we can see that the modifications in the vaccine (UUU -> UUC) are all <em>synonymous</em>. The vaccine RNA code is different, but the same amino acids and the same protein come out.
|
||||
</p><p>
|
||||
If we look closely, we see that the majority of the changes happen in the third codon position, noted with a ‘3’ above. And if we check the universal codon table, we see that this third position indeed often does not matter for which amino acid is produced.
|
||||
</p><p>
|
||||
So, the changes are synonymous, but then why are they there? Looking closely, we see that all changes <em>except one</em> lead to more C and Gs.
|
||||
</p><p>
|
||||
So why would you do that? As noted above, our immune system takes a very dim view of ‘exogenous’ RNA, RNA code coming from outside the cell. To evade detection, the ‘U’ in the RNA was already replaced by a Ψ.
|
||||
</p><p>
|
||||
However, it turns out that RNA with <a href="https://www.nature.com/articles/nrd.2017.243" target="_blank">a higher amount</a> of Gs and Cs is also <a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1463026/" target="_blank">converted more efficiently into proteins</a>,
|
||||
</p><p>
|
||||
And this has been achieved in the vaccine RNA by replacing many characters with Gs and Cs wherever this was possible.
|
||||
</p><blockquote><p>
|
||||
I’m slightly fascinated by the <em>one</em> change that did not lead to an additional C or G, the CCA -> CCU modification. If anyone knows the reason, please let me know! Note that I’m aware that some codons are more common than others in the human genome, but <a href="https://journals.plos.org/plosgenetics/article?id=10.1371/journal.pgen.1006024" target="_blank">I also read that this does not influence translation speed a lot</a>. UPDATE: A number of readers <a href="https://twitter.com/noort_zuit/status/1348924353921081344" target="_blank">have pointed out</a> that this change could prevent a “hairpin” in the RNA. You can try this out yourself on the <a href="http://rna.tbi.univie.ac.at/cgi-bin/RNAWebSuite/RNAfold.cgi" target="_blank">RNAFold service</a>.
|
||||
</p></blockquote><h2>
|
||||
The actual Spike protein
|
||||
</h2><p>
|
||||
The next 3777 characters of the vaccine RNA are similarly ‘codon optimized’ to add a lot of C’s and G’s. In the interest of space I won’t list all the code here, but we are going to zoom in on one exceptionally special bit. This is the bit that makes it work, the part that will actually help us return to life as normal:
|
||||
</p><pre><code> * *
|
||||
L D K V E A E V Q I D R L I T G
|
||||
Virus: CUU GAC AAA GUU GAG GCU GAA GUG CAA AUU GAU AGG UUG AUC ACA GGC
|
||||
Vaccine: CUG GAC CCU CCU GAG GCC GAG GUG CAG AUC GAC AGA CUG AUC ACA GGC
|
||||
L D P P E A E V Q I D R L I T G
|
||||
! !!! !! ! ! ! ! ! ! !
|
||||
</code></pre><p>
|
||||
Here we see the usual synonymous RNA changes. For example, in the first codon we see that CUU is changed into CUG. This adds another ‘G’ to the vaccine, which we know helps enhance protein production. Both CUU and CUG encode for the amino acid ‘L’ or Leucine, so nothing changed in the protein.
|
||||
</p><p>
|
||||
When we compare the entire Spike protein in the vaccine, all changes are synonymous like this.. except for two, and this is what we see here.
|
||||
</p><p>
|
||||
The third and fourth codons above represent actual changes. The K and V amino acids there are both replaced by ‘P’ or Proline. For ‘K’ this required three changes (‘!!!’) and for ‘V’ it required only two (‘!!’).
|
||||
</p><p>
|
||||
<strong>It turns out that these two changes enhance the vaccine efficiency enormously</strong>.
|
||||
</p><p>
|
||||
So what is happening here? If you look at a real SARS-CoV-2 particle, you can see the Spike protein as, well, a bunch of spikes:
|
||||
</p><figure><img src="/articles/sars-em.jpg" alt="SARS virus particles (Wikipedia)"/><figcaption>
|
||||
<p>
|
||||
<a href="https://en.wikipedia.org/wiki/Severe_acute_respiratory_syndrome_coronavirus" target="_blank">SARS virus particles</a> (Wikipedia)
|
||||
</p>
|
||||
</figcaption></figure><p>
|
||||
The spikes are mounted on the virus body (‘the nucleocapsid protein’). But the thing is, our vaccine is only generating the spikes itself, and we’re not mounting them on any kind of virus body.
|
||||
</p><p>
|
||||
It turns out that, unmodified, freestanding Spike proteins collapse into a different structure. If injected as a vaccine, this would indeed cause our bodies to develop immunity.. but only against the collapsed spike protein.
|
||||
</p><p>
|
||||
And the real SARS-CoV-2 shows up with the spiky Spike. The vaccine would not work very well in that case.
|
||||
</p><p>
|
||||
So what to do? In <a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5584442/" target="_blank">2017 it was described how putting a double Proline substitution in just the right place</a> would make the SARS-CoV-1 and MERS S proteins take up their ‘pre-fusion’ configuration, even without being part of the whole virus. This works <a href="https://cen.acs.org/pharmaceuticals/vaccines/tiny-tweak-behind-COVID-19/98/i38" target="_blank">because Proline is a very rigid amino acid</a>. It acts as a kind of splint, stabilising the protein in the state we need to show to the immune system.
|
||||
</p><p>
|
||||
The <a href="https://twitter.com/goodwish916" target="_blank">people</a> that <a href="https://twitter.com/KizzyPhD" target="_blank">discovered</a> this should be walking around high-fiving themselves incessantly. Unbearable amounts of smugness should be emanating from them. <a href="https://twitter.com/McLellan_Lab/status/1291077489566142464" target="_blank">And it would all be well deserved</a>.
|
||||
</p><blockquote><p>
|
||||
Update! I have been contacted by the <a href="https://twitter.com/McLellan_Lab/status/1291077489566142464" target="_blank">McLellan lab</a>, one of the groups behind the Proline discovery. They tell me the high-fiving is subdued because of the ongoing pandemic, but they are pleased to have contributed to the vaccines. They also stress the importance of many other groups, workers and volunteers.
|
||||
</p></blockquote><h2>
|
||||
The end of the protein, next steps
|
||||
</h2><p>
|
||||
If we scroll through the rest of the source code, we encounter some small modifications at the end of the Spike protein:
|
||||
</p><pre><code> V L K G V K L H Y T s
|
||||
Virus: GUG CUC AAA GGA GUC AAA UUA CAU UAC ACA UAA
|
||||
Vaccine: GUG CUG AAG GGC GUG AAA CUG CAC UAC ACA UGA UGA
|
||||
V L K G V K L H Y T s s
|
||||
! ! ! ! ! ! ! !
|
||||
</code></pre><p>
|
||||
At the end of a protein we find a ‘stop’ codon, denoted here by a lowercase ’s’. This is a polite way of saying that the protein should end here. The original virus uses the UAA stop codon, the vaccine uses two UGA stop codons, perhaps just for good measure.
|
||||
</p><h2>
|
||||
The 3’ Untranslated Region
|
||||
</h2><p>
|
||||
Much like the ribosome needed some lead-in at the 5’ end, where we found the ‘five prime untranslated region’, at the end of a protein coding region we find a similar construct called the 3’ UTR.
|
||||
</p><p>
|
||||
Many words could be written about the 3’ UTR, but here I quote <a href="https://en.wikipedia.org/wiki/Three_prime_untranslated_region" target="_blank">what the Wikipedia says</a>: “The 3’-untranslated region plays a crucial role in gene expression by influencing the localization, stability, export, and translation efficiency of an mRNA .. <strong>despite our current understanding of 3’-UTRs, they are still relative mysteries</strong>”.
|
||||
</p><p>
|
||||
What we do know is that certain 3’-UTRs are very successful at promoting protein expression. According to the WHO document, the BioNTech/Pfizer vaccine 3’-UTR was picked from “the amino-terminal enhancer of split (AES) mRNA and the mitochondrial encoded 12S ribosomal RNA to confer RNA stability and high total protein expression”. To which I say, well done.
|
||||
</p><figure><img src="/articles/vaccine.jpg"/></figure><h2>
|
||||
The AAAAAAAAAAAAAAAAAAAAAA end of it all
|
||||
</h2><p>
|
||||
The very end of mRNA is polyadenylated. This is a fancy way of saying it ends on a lot of AAAAAAAAAAAAAAAAAAA. Even mRNA has had enough of 2020 it appears.
|
||||
</p><p>
|
||||
mRNA can be reused many times, but as this happens, it also loses some of the A’s at the end. Once the A’s run out, the mRNA is no longer functional and gets discarded. In this way, the ‘poly-A’ tail is protection from degradation.
|
||||
</p><p>
|
||||
Studies have been done to find out what the optimal number of A’s at the end is for mRNA vaccines. I read in the open literature that this peaked at 120 or so.
|
||||
</p><p>
|
||||
The BNT162b2 vaccine ends with:
|
||||
</p><pre><code> ****** ****
|
||||
UAGCAAAAAA AAAAAAAAAA AAAAAAAAAA AAAAGCAUAU GACUAAAAAA AAAAAAAAAA
|
||||
AAAAAAAAAA AAAAAAAAAA AAAAAAAAAA AAAAAAAAAA AAAAAAAAAA AAAA
|
||||
</code></pre><p>
|
||||
This is 30 A’s, then a “10 nucleotide linker” (GCAUAUGACU), followed by another 70 A’s.
|
||||
</p><p>
|
||||
There are various theories why this linker is there. Some people tell me it has to do with DNA plasmid stability, I have also received this from an actual expert:
|
||||
</p><p>
|
||||
“The 10-nucleotide linker within the poly(A) tail makes it easier to stitch together the synthetic DNA fragments that become the template for transcribing the mRNA. It also reduces slipping by T7 RNA polymerase so that the transcribed mRNA is more uniform in length”.
|
||||
</p><h2>
|
||||
Summarising
|
||||
</h2><p>
|
||||
With this, we now know the exact mRNA contents of the BNT162b2 vaccine, and for most parts we understand why they are there:
|
||||
</p><ul><li>The CAP to make sure the RNA looks like regular mRNA</li><li>A known successful and optimized 5’ untranslated region (UTR)</li><li>A codon optimized signal peptide to send the Spike protein to the right place (amino acids copied 100% from the original virus)</li><li>A codon optimized version of the original spike, with two ‘Proline’ substitutions to make sure the protein appears in the right form</li><li>A known successful and optimized 3’ untranslated region</li><li>A poly-A tail with a ‘linker’ in there</li></ul><p>
|
||||
The codon optimization adds a lot of G and C to the mRNA. Meanwhile, using Ψ (1-methyl-3’-pseudouridylyl) instead of U helps evade our immune system, so the mRNA stays around long enough so we can actually help train the immune system.
|
||||
</p><h2>
|
||||
Further reading/viewing
|
||||
</h2><p>
|
||||
In 2017 I held a two hour presentation on DNA, which you can <a href="https://berthub.eu/dna" target="_blank">view here</a>. Like this page it is aimed at computer people.
|
||||
</p><p>
|
||||
In addition, I’ve been maintaining a page on ‘<a href="https://berthub.eu/articles/posts/amazing-dna/" target="_blank">DNA for programmers</a>’ since 2001.
|
||||
</p><p>
|
||||
Finally, <a href="https://berthub.eu/articles" target="_blank">this listing of my blog posts</a> has quite some DNA, SARS-CoV-2 and COVID related material.
|
||||
</p><p>
|
||||
As an update, the other up and coming vaccines are described in <a href="https://berthub.eu/articles/posts/genetic-code-of-covid-19-vaccines/" target="_blank">The Genetic Code and Proteins of the Other Covid-19 Vaccines</a>
|
||||
</p></div>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<div><p>
|
||||
<span>Published: May 11, 2022</span> <span>By Rosemary Scott</span>
|
||||
</p><img alt="BabyFeet/Getty" src="https://admin.biospace.com/getasset/bae8d5ab-791d-40ac-b9ed-2e44427100dd/?&Test=Test"/><p dir="ltr">
|
||||
Sudden infant death syndrome (SIDS) accounts for about 37% of sudden unexpected infant deaths a year in the U.S., and the cause of SIDS has remained largely unknown. On Saturday, researchers from The Children's Hospital Westmead in Sydney <strong><a href="https://www.sciencedirect.com/science/article/pii/S2352396422002225">released</a></strong> a study that confirmed not only how these infants die, but why.
|
||||
</p><p dir="ltr">
|
||||
SIDS refers to the unexplained deaths of infants under a year old, and it usually occurs while the child is sleeping. According to <em>Mayo Clinic</em>, many in the medical community <strong><a href="https://www.mayoclinic.org/diseases-conditions/sudden-infant-death-syndrome/symptoms-causes/syc-20352800#:~:text=SIDS%20is%20sometimes%20known%20as,breathing%20and%20arousal%20from%20sleep.">suspected</a></strong> this phenomenon could be caused by a defect in the part of the brain that controls arousal from sleep and breathing. The theory was that if the infant stopped breathing during sleep, the defect would keep the child from startling or waking up.
|
||||
</p><p dir="ltr">
|
||||
The Sydney researchers were able to confirm this theory by analyzing dried blood samples taken from newborns who died from SIDS and other unknown causes. Each SIDS sample was then compared with blood taken from healthy babies. They found the activity of the enzyme butyrylcholinesterase (BChE) was significantly lower in babies who died of SIDS compared to living infants and other non-SIDS infant deaths. BChE plays a major role in the brain’s arousal pathway, explaining why SIDS typically occurs during sleep.
|
||||
</p><p dir="ltr">
|
||||
Previously, parents were told SIDS could be prevented if they took proper precautions: laying babies on their backs, not letting them overheat and keeping all toys and blankets out of the crib were a few of the most important preventative steps.
|
||||
</p><p dir="ltr">
|
||||
While safe sleep practices are still important for protecting infants, many children whose parents took every precaution still died from SIDS. These parents were left with immense guilt, wondering if they could have prevented their baby’s death.
|
||||
</p><p dir="ltr">
|
||||
Dr. Carmel Harrington, the lead researcher for the study, was one of these parents. Her son unexpectedly and suddenly died as an infant 29 years ago. In an interview with the <em>Australian Broadcasting Corporation (ABC)</em>, Harrington <strong><a href="https://www.abc.net.au/news/2022-05-08/sydney-researchers-find-enzyme-marker-could-prevent-sids/101047156">explained</a></strong> what she was told about the cause of her child’s death.
|
||||
</p><p dir="ltr">
|
||||
"Nobody could tell me. They just said it's a tragedy. But it was a tragedy that didn't sit well with my scientific brain.”
|
||||
</p><p dir="ltr">
|
||||
Since then, she’s worked to find the cause of SIDS, both for herself and for the medical community as a whole. She went on to explain why this discovery is so important for parents whose babies suffered from SIDS.
|
||||
</p><p>
|
||||
"These families can now live with the knowledge that this was not their fault," she said.
|
||||
</p><p dir="ltr">
|
||||
In the study, the researchers wrote, “This finding represents the possibility for the identification of infants at risk for SIDS infants prior to death and opens new avenues for future research into specific interventions.”
|
||||
</p><p>
|
||||
As the cause is now known, researchers can turn their attention to a solution. In the next few years, those in the medical community who have studied SIDS will likely work on a screening test to identify babies who are at risk for SIDS and hopefully prevent it altogether.
|
||||
</p></div>
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
<div><div>
|
||||
<time datetime="2020-11-06"><span>Nov</span> <span>6</span></time>
|
||||
</div><h2>
|
||||
Rust vs Go
|
||||
</h2><figure><img src="https://images.squarespace-cdn.com/content/v1/5e10bdc20efb8f0d169f85f9/1604661052496-1JWECFMKSRR7UKSHE1L2/ke17ZwdGBToddI8pDm48kBfoYxOA_pZe3DaZs9RVJSIUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYxCRW4BPu10St3TBAUQYVKcIW6kmdqM2CDGG1rvMQo1JpzhjrOryQb_D7KvZjluOzGfODKIY3A_js2Krrat3vB0/rust-vs-go-wide.png" alt="rust-vs-go-wide.png"/></figure><p>
|
||||
Which is better, Rust or Go? Which language should you choose for your next project, and why? How do the two compare in areas like performance, simplicity, safety, features, scale, and concurrency? What do they have in common, and where do they fundamentally differ? Let's find out, in this friendly and even-handed comparison of Rust and Golang, from the author of the <a href="https://bitfieldconsulting.com/books/">For the Love of Go</a> book series.
|
||||
</p><h2>
|
||||
Rust and Go are both awesome
|
||||
</h2><p>
|
||||
First, it's really important to say that <em>both Go and Rust are absolutely excellent programming languages</em>. They're modern, powerful, widely-adopted, and offer excellent performance. You may have read articles and blog posts aiming to convince you that Go is better than Rust, or vice versa. But that really makes no sense; every programming language represents a set of trade-offs. Each language is optimised for different things, so your choice of language should be determined by what suits you and the problems you want to solve with it.
|
||||
</p><p>
|
||||
In this article, I'll try to give a brief overview of where I think Go is the ideal choice, and where I think Rust is a better alternative. Ideally, though, you should have a working familiarity with both languages. While they're very different in syntax and style, both Rust and Go are first-class tools for building software. With that said, let's take a closer look at the two languages.
|
||||
</p><h2>
|
||||
The similarities
|
||||
</h2><p>
|
||||
Rust and Go have a lot in common, which is one reason you often hear them mentioned together. What are some of the common goals of both languages?
|
||||
</p><blockquote><p>
|
||||
Rust is a low-level statically-typed multi-paradigm programming language that’s focused on safety and performance.<br/>
|
||||
—<a href="https://serokell.io/blog/rust-guide">Gints Dreimanis</a>
|
||||
</p><p>
|
||||
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.<br/>
|
||||
—<a href="https://golang.org/">Golang.org</a>
|
||||
</p></blockquote><h3>
|
||||
Memory safety
|
||||
</h3><p>
|
||||
Both Go and Rust belong to the group of modern programming languages whose priority is <em>memory safety</em>. It's become clear over many decades of using older languages such as C and C++ that one of the biggest causes of bugs and security vulnerabilities is accessing memory unsafely or incorrectly. Rust and Go deal with this problem in different ways, but both aim to be smarter and safer than other languages about managing memory, and to help you write correct and performant programs.
|
||||
</p><h3>
|
||||
Fast, compact executables
|
||||
</h3><p>
|
||||
They're both compiled languages, which means your programs are translated directly to executable machine code, so that you can deploy your program as a single binary file; unlike interpreted languages such as <a href="golang/go-vs-python">Python</a> and Ruby, you don't need to distribute an interpreter and lots of libraries and dependencies along with your program, which is a big plus. This also makes both Rust and Go programs extremely fast in comparison to interpreted languages.
|
||||
</p><h3>
|
||||
General-purpose languages
|
||||
</h3><p>
|
||||
Rust and Go are also both powerful, scalable general-purpose programming languages, which you can use to develop all kinds of modern software, from web applications to distributed microservices, or from embedded microcontrollers to mobile apps. Both have excellent standard libraries and a thriving third-party ecosystem, as well as great commercial support and a large user base. They've both been around for many years, and will continue to be widely used for years to come. Learning either Go or Rust today will be a sound investment of your time and effort.
|
||||
</p><h3>
|
||||
Pragmatic programming style
|
||||
</h3><p>
|
||||
Neither are primarily functional languages (like Scala or Elixir, for example), and neither are exclusively object-oriented (like Java and C#). Instead, while both Go and Rust have features associated with functional and object-oriented programming, they're pragmatic languages aimed at solving problems in whatever way is most appropriate, rather than forcing you into a particular way of doing things. (If you like the functional style of programming, though, you’ll find a lot more facilities for it in Rust, because Rust <em>has</em> a lot more facilities than Go in general.)
|
||||
</p><blockquote><p>
|
||||
We can debate about what an ‘object-oriented’ language is, but it’s fair to say that the <em>style</em> of object-oriented programming that C++, Java, or C# users would expect is not present in either Go or Rust.<br/>
|
||||
—Jack Mott
|
||||
</p></blockquote><h3>
|
||||
Development at scale
|
||||
</h3><p>
|
||||
Both Rust and Go have some useful features which make them suitable for programming in the large, whether that means large teams, or large codebases, or both.
|
||||
</p><p>
|
||||
For example, whereas C programmers have argued for years about where to put their brackets, and whether code should be indented with tabs or spaces, both Rust and Go eliminate such issues completely by using a standard formatting tool (<code>gofmt</code> for Go, <code>rustfmt</code> for Rust) which rewrites your code automatically using the canonical style. It’s not that this particular style is so wonderful in itself: it’s the <em>standardisation</em> which Rust and Go programmers appreciate.
|
||||
</p><blockquote><p>
|
||||
<code>gofmt</code>’s style is no one’s favourite, yet <code>gofmt</code> is everyone’s favourite.<br/>
|
||||
—<a href="https://www.youtube.com/watch?v=PAAkCSZUG1c&t=8m43s">Rob Pike</a>
|
||||
</p></blockquote><p>
|
||||
Another area where both languages score highly is in the build pipeline. Both have excellent, built-in, high-performance standard build and dependency management tools; no more wrestling with complex third-party build systems and having to learn a new one every couple of years.
|
||||
</p><blockquote><p>
|
||||
Building Go and Rust code, having come from a Java and Ruby background in my early career, felt like an impossible weight off my shoulders. When I was at Google, it was a relief to come across a service that was written in Go, because I knew it would be easy to build and run. This has also been true of Rust, though I've only worked on that at much smaller scale. I'm hoping that the days of infinitely configurable build systems are dead, and languages all ship with their own purpose-built build tools that just work out of the box.<br/>
|
||||
—<a href="https://samwho.dev/">Sam Rose</a>
|
||||
</p></blockquote><h2>
|
||||
So what’s all the fuss about?
|
||||
</h2><p>
|
||||
With all that in mind, and seeing that both languages are so well-designed and powerful, you might be wondering what all the holy wars are about (me too). Why do people make such a fuss about 'Go versus Rust', getting into angry social media spats and writing long blog posts about how only an idiot would use Rust, or that Go isn't a real programming language, or whatever. It might make them feel better, but it doesn't exactly help you, as someone trying to decide which language to use for your project, or which you should learn to advance your programming career. A wise person doesn't make important choices based on who shouts the loudest.
|
||||
</p><p>
|
||||
Let's continue our grown-up discussion now by looking at some areas where a reasonable person might prefer one language over the other.
|
||||
</p><h2>
|
||||
Performance
|
||||
</h2><p>
|
||||
We've said that both Go and Rust produce extremely fast programs because they're compiled to native machine code, without having to go through an interpreter or virtual machine. However, Rust's performance is particularly outstanding. It's comparable with C and C++, which are often regarded as the highest-performance compiled languages, but unlike these older languages, it also offers memory safety and concurrency safety at essentially no cost in execution speed. Rust also allows you to create complex abstractions without paying a performance penalty at run-time.
|
||||
</p><p>
|
||||
By comparison, although Go programs also perform extremely well, Go is primarily designed for speed of <em>development</em> (including compilation), rather than speed of execution. The Go compiler doesn’t spend a lot of time trying to generate the most efficient possible machine code; it cares more about compiling lots of code quickly. So Rust will usually beat Go in run-time benchmarks.
|
||||
</p><p>
|
||||
Rust’s run-time performance is also consistent and predictable, because it doesn’t use garbage collection. Go’s garbage collector is very efficient, and it’s optimised to make its stop-the-world pauses as short as possible (and getting shorter with every new Go release). But garbage collection inevitably introduces some unpredictability in the way programs behave, which can be a serious issue in some applications, such as embedded systems.
|
||||
</p><p>
|
||||
Because Rust aims to give the programmer complete control of the underlying hardware, it's possible to optimise Rust programs to be pretty close to the maximum theoretical performance of the machine. This makes Rust an excellent choice for areas where speed of execution beats all other considerations, such as game programming, operating system kernels, web browser components, and real-time control systems.
|
||||
</p><h2>
|
||||
Simplicity
|
||||
</h2><p>
|
||||
It doesn't matter how fast a programming language is if nobody can figure out how to use it. Go was deliberately conceived as a reaction against the ever-growing complexity of languages like C++; it has very little syntax, very few keywords, and, indeed, few features. This means it doesn't take long to learn the Go language to the point where you can write useful programs in it.
|
||||
</p><blockquote><p>
|
||||
Go is incredibly easy to learn. I know this is an often-touted benefit, but I was really surprised at how quickly I was able to be productive. Thanks to the language, docs, and tools, I was writing interesting, committable code after literally two days.<br/>
|
||||
—<a href="https://medium.com/better-programming/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410">Early Impressions of Go From a Rust Programmer</a>
|
||||
</p></blockquote><p>
|
||||
The key word here is <em>simplicity</em>. Simple isn't the same as easy, for sure, but a small, simple language is easier to learn than a large, complex one. There just aren't that many different ways to do things, so all well-written Go code tends to look the same. It’s easy to just dive into an unfamiliar service and understand what it’s doing.
|
||||
</p><pre><code>fmt.Println("Gopher's Diner Breakfast Menu")
|
||||
for dish, price := range menu {
|
||||
fmt.Println(dish, price)
|
||||
}</code></pre><p>
|
||||
(In the <a href="/videos">Code Club</a> series, I do exactly that: pick Go projects semi-randomly from GitHub and explore them with a group of Go beginners, to see how much of the code we can understand. It always turns out to be more than we expected!)
|
||||
</p><p>
|
||||
Although the core language is small, Go’s standard library is very powerful. That means your learning curve will also need to include the parts of the standard library that you need, not just Go syntax. On the other hand, moving functionality out of the language and into the standard library means that you can focus on learning only the libraries that are relevant to you right now.
|
||||
</p><p>
|
||||
Go is also designed for software development at scale, with large codebases and large teams. In these situations, it's important that new developers can get up to speed as quickly as possible.
|
||||
</p><blockquote><p>
|
||||
With Go, you get things done—fast. Go is one of the most productive languages I've ever worked with. The mantra is: <em>solve real problems today</em>.<br/>
|
||||
—<a href="https://endler.dev/2017/go-vs-rust/">Matthias Endler</a>
|
||||
</p></blockquote><h2>
|
||||
Features
|
||||
</h2><blockquote><p>
|
||||
Rust supports more complexity than several other programming languages, therefore, you can achieve more with it. For example, it supports generics.<br/>
|
||||
—<a href="https://devathon.com/blog/rust-vs-go-which-programming-language-to-choose/">Devathon</a>
|
||||
</p></blockquote><p>
|
||||
Rust is specifically designed to include lots of powerful and useful features to help programmers do the most with the least code. For example, Rust's <code>match</code> feature lets you write flexible, expressive logic quite concisely:
|
||||
</p><pre><code>fn is_prime(n: u64) -> bool {
|
||||
match n {
|
||||
0...1 => false,
|
||||
_ => !(2..n).any(|d| n % d == 0),
|
||||
}
|
||||
}</code></pre><p>
|
||||
Because Rust <em>does</em> a lot, this means there's a lot to learn, especially at the start. But that's okay: there's a lot to learn in C++ or Java, too, and you don't get the advanced features that come with Rust, like memory safety. To criticise Rust for being a complex language misses the point: it's designed to be expressive, which means having a lot of features, and in many situations that's what you want from a programming language. There's a learning curve, for sure, but once you’re up and running with it, you’ll be fine.
|
||||
</p><blockquote><p>
|
||||
Rust competes for mindshare with C++ and D for programmers who are prepared to accept more complex syntax and semantics (and presumably higher readability costs) in return for the maximum possible performance.<br/>
|
||||
—<a href="https://dave.cheney.net/2015/07/02/why-go-and-rust-are-not-competitors">Dave Cheney</a>
|
||||
</p></blockquote><h2>
|
||||
Concurrency
|
||||
</h2><p>
|
||||
Most languages have some form of support for concurrent programming (doing multiple things at once), but Go was designed for this job from the ground up. Instead of using operating system threads, Go provides a lightweight alternative: <em>goroutines</em>. Each goroutine is an independently executing Go function, which the Go scheduler will map to one of the OS threads under its control. This means that the scheduler can very efficiently manage a large number of concurrent goroutines, using only a limited number of OS threads.
|
||||
</p><p>
|
||||
Consequently, you can run millions of concurrent goroutines in a single program without creating serious performance problems. That makes Go the perfect choice for high-scale concurrent applications such as webservers and microservices.
|
||||
</p><p>
|
||||
Go also features fast, safe, efficient ways for goroutines to communicate and share data, using <em>channels</em>. Go's concurrency support feels well-designed, and a pleasure to use. Reasoning about concurrent programs is hard in general, and building reliable, correct concurrent programs is a challenge in any language. Because it was built into the language from the start, though, instead of being an afterthought, concurrent programming in Go is about as simple and well-integrated as it could reasonably be.
|
||||
</p><blockquote><p>
|
||||
Go makes it very easy to build a nicely factored application that takes full advantage of concurrency while being deployed as a set of microservices. Rust can do those things, too, but it’s arguably a bit tougher. In some respects, Rust’s obsession with preventing memory-related security vulnerabilities means that programmers have to go out of their way to perform tasks that would be simpler in other languages, including Go.<br/>
|
||||
—<a href="https://sdtimes.com/softwaredev/the-developers-dilemma-choosing-between-go-and-rust/">Sonya Koptyev</a>
|
||||
</p></blockquote><p>
|
||||
The concurrency story in Rust is very new, by comparison, and still stabilising, but it’s under very active development, so watch this space. For example, Rust’s <a href="https://github.com/rayon-rs/rayon">rayon</a> library provides a very elegant and lightweight way of turning sequential computations into parallel ones.
|
||||
</p><blockquote><p>
|
||||
Having lightweight syntax for spawning Go routines and using channels are really nice. It really shows the power of syntax that such small details make concurrent programming feel so much nicer than in other languages.<br/>
|
||||
—<a href="https://medium.com/better-programming/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410">Early Impressions of Go From a Rust Programmer</a>
|
||||
</p></blockquote><p>
|
||||
While it may be a bit less straightforward to implement concurrent programs in Rust, it’s still possible, and those programs can take advantage of Rust’s guarantees about safety. A good example is the standard library’s <code>Mutex</code> class: in Go, you can forget to obtain a mutex lock before accessing something, but Rust won’t let you do that.
|
||||
</p><blockquote><p>
|
||||
Go is focused on concurrency as a first class concept. That is not to say you cannot find aspects of Go’s actor oriented concurrency in Rust, but it is left as an exercise to the programmer.<br/>
|
||||
—<a href="https://dave.cheney.net/2015/07/02/why-go-and-rust-are-not-competitors">Dave Cheney</a>
|
||||
</p></blockquote><h2>
|
||||
Safety
|
||||
</h2><p>
|
||||
We saw earlier that both Go and Rust aim, in different ways, to prevent a large class of common programming errors, to do with memory management. But Rust in particular goes to great lengths to ensure that you can't do something unsafe that you didn't mean to do.
|
||||
</p><blockquote><p>
|
||||
Rust's very strict and pedantic compiler checks each and every variable you use and every memory address you reference. It avoids possible data race conditions and informs you about undefined behavior. Concurrency and memory safety issues are fundamentally impossible to get in the safe subset of Rust.<br/>
|
||||
—<a href="https://bitbucket.org/blog/why-rust">Why Rust?</a>
|
||||
</p></blockquote><p>
|
||||
This is going to make programming in Rust a different experience to almost all other languages, and it may be a challenging one at first. But for many people, the hard work is worth it.
|
||||
</p><blockquote><p>
|
||||
For me the key advantage of Rust is a feeling that the compiler has my back and won't let through any bug it could possibly detect (seriously, it feels like magic sometimes).<br/>
|
||||
—Grzegorz Nosek
|
||||
</p></blockquote><p>
|
||||
Many languages, including Go, have facilities to help programmers avoid mistakes, but Rust takes this to a new level, so that potentially incorrect programs won’t even compile.
|
||||
</p><blockquote><p>
|
||||
With Rust, the library programmer has a lot of tools to prevent her users making mistakes. Rust gives us the ability to say that we <em>own</em> a specific piece of data; it's not possible for anything else to claim ownership, so we know nothing else will be able to modify it. I can't think of a time I've ever been given this many tools to prevent accidental misuse before. It's a wonderful feeling.<br/>
|
||||
—<a href="https://samwho.dev/">Sam Rose</a>
|
||||
</p></blockquote><p>
|
||||
"Fighting with the borrow checker" is a common syndrome for new Rust programmers, but in most cases the problems that it finds are genuine bugs (or at least potential bugs) in your code. It may force you to fundamentally re-architect your program to avoid running into these issues; and that's a good thing, when correctness and reliability are your top priority. What's the point of a language that doesn't change the way you program? The lessons that Rust teaches about safety can be useful when you’re working in other languages, too.
|
||||
</p><blockquote><p>
|
||||
If you choose Rust, usually you need the guarantees that the language provides: safety against null pointers and data races, predictable runtime behaviour, and total control over the hardware. If you don't require any of these features, Rust might be a poor choice for your next project. That's because these guarantees come with a cost: ramp-up time. You'll need to unlearn bad habits and learn new concepts. Chances are, you will fight with the borrow checker a lot when you start out.<br/>
|
||||
—<a href="https://endler.dev/2017/go-vs-rust/">Matthias Endler</a>
|
||||
</p></blockquote><p>
|
||||
How challenging you find Rust's programming model probably depends on what previous experience you have in other languages. Python or Ruby programmers may find it restrictive; others will be delighted.
|
||||
</p><blockquote><p>
|
||||
If you’re a C or C++ programmer who’s spent weeks chasing down memory safety bugs in those languages, you’ll really appreciate Rust. “Fighting the borrow checker” becomes “The compiler can detect that? Cool!”<br/>
|
||||
—Grzegorz Nosek
|
||||
</p></blockquote><h2>
|
||||
Scale
|
||||
</h2><blockquote><p>
|
||||
Today's server programs comprise tens of millions of lines of code, are worked on by hundreds or even thousands of programmers, and are updated literally every day. Go was designed and developed to make working in this environment more productive. Go's design considerations include rigorous dependency management, the adaptability of software architecture as systems grow, and robustness across the boundaries between components.<br/>
|
||||
—<a href="https://talks.golang.org/2012/splash.article">Rob Pike</a>
|
||||
</p></blockquote><p>
|
||||
When you're working on a problem by yourself or in small teams, the choice of a simple language or a rich language is a matter of preference. But as the software grows bigger and more complex, and the teams grow larger, the differences really start to show. For large applications and distributed systems, speed of execution is less important than speed of development: a deliberately minimal language like Go reduces the ramp-up time for new developers, and makes it easier for them to work with a large codebase.
|
||||
</p><blockquote><p>
|
||||
With Go, it’s easier as a junior developer to be more productive, and harder as a mid-level developer to introduce brittle abstractions that will cause problems down the line. For these reasons, Rust is less compelling than Go for enterprise software development.<br/>
|
||||
—<a href="https://kristoff.it/blog/why-go-and-not-rust">Loris Cro</a>
|
||||
</p></blockquote><p>
|
||||
When it comes to software development in the large, clear is better than clever. Go's limitations actually make it more suitable for enterprises and big organisations than more complex and powerful languages such as Rust.
|
||||
</p><h2>
|
||||
The differences
|
||||
</h2><p>
|
||||
Although Rust and Go are both popular, modern, widely-used languages, they're not really competitors, in the sense that they're deliberately targeting quite different use cases. Go's whole approach to programming is radically different to Rust's, and each language will suit some people while irritating others. That's absolutely fine, and if both Rust and Go did more or less the same things in more or less the same way, we wouldn't really need two different languages.
|
||||
</p><p>
|
||||
So can we get a sense of the respective natures of Rust and Go by finding issues on which they take drastically different approaches? Let's find out.
|
||||
</p><h3>
|
||||
Garbage collection
|
||||
</h3><p>
|
||||
“To garbage-collect, or not to garbage-collect” is one of those questions that has no right answer. Garbage collection, and automatic memory management in general, makes it quick and easy to develop reliable, efficient programs, and for some people that makes it essential. But others say that garbage collection, with its performance overhead and stop-the-world pauses, makes programs behave unpredictably at run-time, and introduces unacceptable latency. The debate rumbles on.
|
||||
</p><blockquote><p>
|
||||
Go is a very different language to Rust. Although both can vaguely be described as systems languages or replacements for C, they have different goals and applications, styles of language design, and priorities. Garbage collection is a really huge differentiator. Having GC in Go makes the language much simpler and smaller, and easy to reason about.
|
||||
</p><p>
|
||||
Not having GC in Rust makes it really fast (especially if you need guaranteed latency, not just high throughput) and enables features and programming patterns that are not possible in Go (or at least not without sacrificing performance).<br/>
|
||||
—<a href="https://medium.com/better-programming/early-impressions-of-go-from-a-rust-programmer-f4fd1074c410">PingCAP</a>
|
||||
</p></blockquote><h3>
|
||||
Close to the metal
|
||||
</h3><p>
|
||||
The history of computer programming has been a story of increasingly sophisticated abstractions that let the programmer solve problems without worrying too much about how the underlying machine actually works. That makes programs easier to write and perhaps more portable. But for many programs, access to the hardware, and precise control of how the program is executed, are more important. Rust aims to let programmers get “closer to the metal”, with more control, but Go abstracts away the architectural details to let programmers get closer to the problem.
|
||||
</p><blockquote><p>
|
||||
Both languages have a different scope. Golang shines for writing microservices and for typical "DevOps" tasks, but it is not a systems programming language. Rust is stronger for tasks where concurrency, safety and/or performance are important; but it has a steeper learning curve than Go.<br/>
|
||||
—<a href="https://endler.dev/2017/go-vs-rust/">Matthias Endler</a>
|
||||
</p></blockquote><h3>
|
||||
Must go faster
|
||||
</h3><p>
|
||||
I’ve written elsewhere that <a href="https://bitfieldconsulting.com/golang/slower">performance is less important than readability for most programs</a>. But when performance <em>does</em> matter, it really matters. Rust makes a number of design trade-offs to achieve the best possible execution speed. By contrast, Go is more concerned about simplicity, and it’s willing to sacrifice some (run-time) performance for it. But Go’s <em>build</em> speed is unbeatable, and that’s important for large codebases.
|
||||
</p><blockquote><p>
|
||||
Rust is faster than Go. In the benchmarks above, Rust was faster, and in some cases, an order of magnitude faster. But before you run off choosing to write everything in Rust, consider that Go wasn’t that far behind it in many of those benchmarks, and it’s still much faster than the likes of Java, C#, JavaScript, Python and so on.
|
||||
</p><p>
|
||||
If what you need is top-of-the-line performance, then you’ll be ahead of the game choosing either of these two languages. If you’re building a web service that handles high load, that you want to be able to scale both vertically and horizontally, either language will suit you perfectly.<br/>
|
||||
—<a href="https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9">Andrew Lader</a>
|
||||
</p></blockquote><h3>
|
||||
Correctness
|
||||
</h3><p>
|
||||
On the other hand, a program can be arbitrarily fast if it doesn’t have to work properly. Most code is not written for the long term, but it’s often surprising how long some programs can stay running in production: in some cases, many decades. In these situations it’s worth taking a little extra time in development to make sure that the program is correct, reliable, and won’t need a lot of maintenance in the future.
|
||||
</p><blockquote><p>
|
||||
My take: Go for the code that has to ship tomorrow, Rust for the code that has to keep running untouched for the next five years.<br/>
|
||||
—Grzegorz Nosek
|
||||
</p></blockquote><p>
|
||||
While both Go and Rust are great choices for any serious project, it's a good idea to make yourself as well-informed as possible about each language and its characteristics. Ultimately, it doesn't matter what anyone else thinks: only you can decide which is right for you and your team.
|
||||
</p><blockquote><p>
|
||||
If you want to develop faster, perhaps because you have many different services to write, or you have a large team of developers, then Go is your language of choice. Go gives you concurrency as a first-class citizen, and does not tolerate unsafe memory access (neither does Rust), but without forcing you to manage every last detail. Go is fast and powerful, but it avoids bogging the developer down, focusing instead on simplicity and uniformity. If on the other hand, wringing out every last ounce of performance is a necessity, then Rust should be your choice.<br/>
|
||||
—<a href="https://codeburst.io/should-i-rust-or-should-i-go-59a298e00ea9">Andrew Lader</a>
|
||||
</p></blockquote><h2>
|
||||
Conclusion
|
||||
</h2><p>
|
||||
I hope this article has convinced you that <em>both</em> Rust and Go deserve your serious consideration. If at all possible, you should aim to get at least some level of experience in both languages, because they will be incredibly useful to you in any tech career, or even if you enjoy programming as a hobby. If you only have time to invest in learning one language well, don't make your final decision until you've used both Go and Rust for a variety of different kinds of programs, large and small.
|
||||
</p><p>
|
||||
And knowledge of a programming language is really only a small part of being a successful software engineer. By far the most important skills you'll need are design, engineering, architecture, communication, and collaboration. If you excel at these, you'll be a great software engineer regardless of your choice of language. Happy learning!
|
||||
</p><h2>
|
||||
Comparing Rust and Go code
|
||||
</h2><p>
|
||||
There's a great website called <a href="programming-idioms.org">programming-idioms.org</a> which has a "cheat sheet" showing what the Rust and Go code looks like for over 200 common programming tasks:
|
||||
</p></div>
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
<div><img src="https://blog.jetbrains.com/wp-content/uploads/2021/06/avatar-0.5x-200x200.png" alt="Khalid Abuhakmeh" loading="lazy"/><p>
|
||||
You’ve likely heard about <a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor">Blazor</a>, .NET’s frontend framework for writing web applications. Blazor lets you use your existing C# skills to build full-stack applications, from client to server. One way to construct Blazor applications is to compile your frontend into a WebAssembly (WASM) file and deploy and run your app in a web browser. The Blazor WASM approach can help you build client experiences similar to native desktop applications with a native performance profile.
|
||||
</p><p>
|
||||
But what about WebAssembly on the server? In this post, we’ll see the benefits of using WASM outside the browser and its potential as a transformative technology on the server.
|
||||
</p><h2>
|
||||
What is WebAssembly?
|
||||
</h2><p>
|
||||
WebAssembly is a low-level assembly-like language with a binary format that runs in modern web browsers. As a result, WebAssembly programs can perform at near-native speeds allowing for new and exciting applications for web clients from desktop-like apps, emulators, high-performance video games, photo editors, and more. The additional benefit of WebAssembly is it can be run either as an extra add-on via service workers, enhancing or replacing the existing JavaScript experience on the client.
|
||||
</p><p>
|
||||
The designers of WebAssembly always meant it to run alongside the current web model. Developers unfamiliar with JavaScript can choose from a wide range of languages to write WebAssembly, including C, C++, C#, Go, Python and Rust, with many more languages looking at targeting WASM as an artifact of compilation. For .NET developers, you can think of the <code>.wasm</code> file extension as a <code>.dll</code> or <code>.exe</code> format. The file format is a standard, and WebAssembly System Interface (WASI) compliant runtime will be able to consume them.
|
||||
</p><p>
|
||||
One such runtime we’ll be exploring in this post is <strong><a href="https://wasmtime.dev/">Wasmtime</a></strong>.
|
||||
</p><h2>
|
||||
What is Wasmtime?
|
||||
</h2><p>
|
||||
Developed by the <a href="https://bytecodealliance.org/">Bytecode Alliance</a>, <a href="https://wasmtime.dev/">Wasmtime is a runtime for WebAssembly</a>, allowing you to consume <code>.wasm</code> files and run them on Windows, macOS, or Linux systems outside a web client. Wasmtime is a fast, secure, and standards-compliant runtime for WebAssembly, and supports the <a href="https://wasi.dev/">WebAssembly System Interface (WASI)</a>.
|
||||
</p><p>
|
||||
The most crucial feature of Wasmtime is its ability to interact with the native operating system, allowing for configurable access to system resources. Resources include disk access, TCP listeners, hardware input interfaces, and more.
|
||||
</p><p>
|
||||
In addition to running standalone <code>.wasm</code> files, you can use Wasmtime inside your applications to consume third-party dependencies. The portability of the WASM format opens up a world where you can have native interop with a standard format across all languages.
|
||||
</p><p>
|
||||
Wasmtime also supports debugging using popular native debugging tools like GDB or LLDB, which many of the IntelliJ-family products already support. The debugging experience depends on your technology stack’s build tools.
|
||||
</p><p>
|
||||
Speaking of build tools, let’s build our first WASM server application, but first, let’s install Wasmtime.
|
||||
</p><h2>
|
||||
Installing Wasmtime
|
||||
</h2><p>
|
||||
Wasmtime is a CLI executable, so it’s straightforward to install on any environment. The first step is to visit the <a href="https://wasmtime.dev/">official Bytecode Alliance website</a>. You can also just run the following command.
|
||||
</p><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20d%3D%22M19.436%2C36.875L6.568%2C25.002v-3.863L19.436%2C9.267v5.041l-9.583%2C8.668v0.188l9.583%2C8.669V36.875z%22%2F%3E%0D%0A%09%3Cpath%20d%3D%22M26.343%2C36.875v-5.041l9.583-8.669v-0.188l-9.583-8.668V9.267l12.868%2C11.872v3.863L26.343%2C36.875z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2253.75%22%20y1%3D%2239.353%22%20x2%3D%2286.375%22%20y2%3D%2239.353%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2251.711%22%20y1%3D%2230.534%22%20x2%3D%2284.336%22%20y2%3D%2230.534%22%2F%3E%0D%0A%3Crect%20x%3D%228.932%22%20y%3D%227.334%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.097%22%20height%3D%2224.952%22%2F%3E%0D%0A%3Crect%20x%3D%2218.942%22%20y%3D%2215.424%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.096%22%20height%3D%2224.953%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%229.25%22%20x2%3D%2239.75%22%20y2%3D%229.25%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2218.167%22%20x2%3D%2239.75%22%20y2%3D%2218.167%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2227.083%22%20x2%3D%2239.75%22%20y2%3D%2227.083%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2236%22%20x2%3D%2229.809%22%20y2%3D%2236%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22E%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20fill%3D%22%23202F65%22%20d%3D%22M32.48%2C25.614H19.64l-4.933%2C9.826l17.746%2C0.037l-6.173%2C5.358L8.167%2C40.912L16.29%2C6.055h22.974l-5.734%2C5.354%0D%0A%09%09l-13.306-0.027l0.672%2C8.797h12.841L32.48%2C25.614z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><div>
|
||||
<span>curl https:</span><span>//wasmtime.dev/install.sh -sSf | bash</span>
|
||||
</div><div>
|
||||
curl https://wasmtime.dev/install.sh -sSf | bash
|
||||
</div><pre>curl https://wasmtime.dev/install.sh -sSf | bash</pre><p>
|
||||
Windows users can do the same using the Windows Subsystem for Linux (WSL), or <a href="https://github.com/bytecodealliance/wasmtime/releases">grab the installer from the Wasmtime releases</a> page.
|
||||
</p><p>
|
||||
You can check you’ve successfully installed Wasmtime by running the command <code>wasmtime –version</code> from the command line.
|
||||
</p><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20d%3D%22M19.436%2C36.875L6.568%2C25.002v-3.863L19.436%2C9.267v5.041l-9.583%2C8.668v0.188l9.583%2C8.669V36.875z%22%2F%3E%0D%0A%09%3Cpath%20d%3D%22M26.343%2C36.875v-5.041l9.583-8.669v-0.188l-9.583-8.668V9.267l12.868%2C11.872v3.863L26.343%2C36.875z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2253.75%22%20y1%3D%2239.353%22%20x2%3D%2286.375%22%20y2%3D%2239.353%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2251.711%22%20y1%3D%2230.534%22%20x2%3D%2284.336%22%20y2%3D%2230.534%22%2F%3E%0D%0A%3Crect%20x%3D%228.932%22%20y%3D%227.334%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.097%22%20height%3D%2224.952%22%2F%3E%0D%0A%3Crect%20x%3D%2218.942%22%20y%3D%2215.424%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.096%22%20height%3D%2224.953%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%229.25%22%20x2%3D%2239.75%22%20y2%3D%229.25%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2218.167%22%20x2%3D%2239.75%22%20y2%3D%2218.167%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2227.083%22%20x2%3D%2239.75%22%20y2%3D%2227.083%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2236%22%20x2%3D%2229.809%22%20y2%3D%2236%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22E%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20fill%3D%22%23202F65%22%20d%3D%22M32.48%2C25.614H19.64l-4.933%2C9.826l17.746%2C0.037l-6.173%2C5.358L8.167%2C40.912L16.29%2C6.055h22.974l-5.734%2C5.354%0D%0A%09%09l-13.306-0.027l0.672%2C8.797h12.841L32.48%2C25.614z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><div>
|
||||
<span>></span> <span>wasmtime --version</span>
|
||||
</div><div>
|
||||
<span>wasmtime-cli</span> <span>1.0</span><span>.</span><span>1</span>
|
||||
</div><pre>> wasmtime --version
|
||||
wasmtime-cli 1.0.1</pre><h2>
|
||||
Compiling a WASM .NET application
|
||||
</h2><p>
|
||||
<strong>Note: You’ll need the latest version of .NET 7 SDK installed</strong>
|
||||
</p><p>
|
||||
Our goal in this section is to take a C# Console application and compile it to a <code>.wasm</code> file. The solution will be a self-contained application that won’t need unique WASI resources like TCP listeners, file access, or environment variables.
|
||||
</p><p>
|
||||
First, let’s start with a brand new Console Application project. From JetBrains Rider’s new solution window, select the <strong><em>“Console Application”</em></strong> template and call it <em><strong>“HelloWasm”</strong></em> or whatever you’d like.
|
||||
</p><figure><img src="https://blog.jetbrains.com/wp-content/uploads/2022/10/image.png" alt=""/></figure><p>
|
||||
Once the solution is ready, you’ll need to add the “<strong>Wasi.Sdk”</strong> NuGet package to your console project. If you don’t see the package, be sure that you have the <strong>Prerelease</strong> checkbox checked.
|
||||
</p><figure><img loading="lazy" src="https://blog.jetbrains.com/wp-content/uploads/2022/10/image-1.png" alt=""/><figcaption>Wasi.Sdk Nuget package in JetBrains Rider’s NuGet tool window</figcaption></figure><p>
|
||||
You’re ready to compile the console application into a <code>.wasm</code> artifact. Before doing so, change the line in <code>Program.cs</code> to the following:
|
||||
</p><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20d%3D%22M19.436%2C36.875L6.568%2C25.002v-3.863L19.436%2C9.267v5.041l-9.583%2C8.668v0.188l9.583%2C8.669V36.875z%22%2F%3E%0D%0A%09%3Cpath%20d%3D%22M26.343%2C36.875v-5.041l9.583-8.669v-0.188l-9.583-8.668V9.267l12.868%2C11.872v3.863L26.343%2C36.875z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2253.75%22%20y1%3D%2239.353%22%20x2%3D%2286.375%22%20y2%3D%2239.353%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2251.711%22%20y1%3D%2230.534%22%20x2%3D%2284.336%22%20y2%3D%2230.534%22%2F%3E%0D%0A%3Crect%20x%3D%228.932%22%20y%3D%227.334%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.097%22%20height%3D%2224.952%22%2F%3E%0D%0A%3Crect%20x%3D%2218.942%22%20y%3D%2215.424%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.096%22%20height%3D%2224.953%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%229.25%22%20x2%3D%2239.75%22%20y2%3D%229.25%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2218.167%22%20x2%3D%2239.75%22%20y2%3D%2218.167%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2227.083%22%20x2%3D%2239.75%22%20y2%3D%2227.083%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2236%22%20x2%3D%2229.809%22%20y2%3D%2236%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22E%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20fill%3D%22%23202F65%22%20d%3D%22M32.48%2C25.614H19.64l-4.933%2C9.826l17.746%2C0.037l-6.173%2C5.358L8.167%2C40.912L16.29%2C6.055h22.974l-5.734%2C5.354%0D%0A%09%09l-13.306-0.027l0.672%2C8.797h12.841L32.48%2C25.614z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><div>
|
||||
<span>Console.</span><span>WriteLine</span><span>(</span><span>"Hello, Wasmtime!"</span><span>)</span><span>;</span>
|
||||
</div><div>
|
||||
Console.WriteLine("Hello, Wasmtime!");
|
||||
</div><pre>Console.WriteLine("Hello, Wasmtime!");</pre><p>
|
||||
Building your project, you will see references to bundling System libraries commonly used in .NET applications. Remember that WASM is native byte code. The .NET runtime must be packaged and shipped as part of your artifact. This is not any different than Blazor WASM deployments for a web client.
|
||||
</p><figure><img loading="lazy" src="https://blog.jetbrains.com/wp-content/uploads/2022/10/image-2.png" alt=""/><figcaption>Building a Wasi.Sdk-powered. NET project inside of JetBrains Rider</figcaption></figure><p>
|
||||
Now, let’s run our WASM file using Wasmtime. In Rider, you’ll use the <strong>Run Anything</strong> dialog (Ctrl+Ctrl) to run the following command:
|
||||
</p><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20d%3D%22M19.436%2C36.875L6.568%2C25.002v-3.863L19.436%2C9.267v5.041l-9.583%2C8.668v0.188l9.583%2C8.669V36.875z%22%2F%3E%0D%0A%09%3Cpath%20d%3D%22M26.343%2C36.875v-5.041l9.583-8.669v-0.188l-9.583-8.668V9.267l12.868%2C11.872v3.863L26.343%2C36.875z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2253.75%22%20y1%3D%2239.353%22%20x2%3D%2286.375%22%20y2%3D%2239.353%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2251.711%22%20y1%3D%2230.534%22%20x2%3D%2284.336%22%20y2%3D%2230.534%22%2F%3E%0D%0A%3Crect%20x%3D%228.932%22%20y%3D%227.334%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.097%22%20height%3D%2224.952%22%2F%3E%0D%0A%3Crect%20x%3D%2218.942%22%20y%3D%2215.424%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.096%22%20height%3D%2224.953%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%229.25%22%20x2%3D%2239.75%22%20y2%3D%229.25%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2218.167%22%20x2%3D%2239.75%22%20y2%3D%2218.167%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2227.083%22%20x2%3D%2239.75%22%20y2%3D%2227.083%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2236%22%20x2%3D%2229.809%22%20y2%3D%2236%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22E%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20fill%3D%22%23202F65%22%20d%3D%22M32.48%2C25.614H19.64l-4.933%2C9.826l17.746%2C0.037l-6.173%2C5.358L8.167%2C40.912L16.29%2C6.055h22.974l-5.734%2C5.354%0D%0A%09%09l-13.306-0.027l0.672%2C8.797h12.841L32.48%2C25.614z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><div>
|
||||
<span>wasmtime ./HelloWasm/bin/Debug/net7.</span><span>0</span><span>/HelloWasm.</span><span>wasm</span>
|
||||
</div><div>
|
||||
wasmtime ./HelloWasm/bin/Debug/net7.0/HelloWasm.wasm
|
||||
</div><pre>wasmtime ./HelloWasm/bin/Debug/net7.0/HelloWasm.wasm</pre><p>
|
||||
When successful, you should see your Console application’s output.
|
||||
</p><figure><img loading="lazy" src="https://blog.jetbrains.com/wp-content/uploads/2022/10/image-3.png" alt=""/><figcaption>Successful output in JetBrains Rider’s “Run Anything” tool window</figcaption></figure><p>
|
||||
Awesome! You just wrote your first WASM-targeted .NET application. Now you could deploy the <code>.wasm</code> file to any host that supports WebAssmebly.
|
||||
</p><h2>
|
||||
Consuming WASM and WAT modules from .NET
|
||||
</h2><p>
|
||||
While it’s certainly possible to consume a <code>.wasm</code> file in .NET, it is a bit more tedious than the plug-and-play experience of a NuGet package. Currently, there is a <strong><em>Wasmruntime</em></strong> NuGet package that contains low-level APIs for dealing with the elements of a host. These elements include an <code>Engine</code>, <code>Module</code>, <code>Linker</code>, <code>Store</code>, and more. They allow you to define your custom host, including low-level system calls and what calling them may do on a WASI-compliant system. For more information, I recommend reading excellent <a href="https://developer.mozilla.org/en-US/docs/WebAssembly/Concepts#webassembly_goals">Mozilla documentation to understand how WASM interfaces with a host</a>.
|
||||
</p><p>
|
||||
I would not recommend most developers attempt to write their host; instead, the community should wait for the Byte Code Alliance to create a safe-by-default host.
|
||||
</p><p>
|
||||
Let’s look at an example where we use the WebAssembly Text (WAT) format to link a C# function to WebAssembly. WAT is a human-readable and editable format representing the WASM binary format.
|
||||
</p><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20d%3D%22M19.436%2C36.875L6.568%2C25.002v-3.863L19.436%2C9.267v5.041l-9.583%2C8.668v0.188l9.583%2C8.669V36.875z%22%2F%3E%0D%0A%09%3Cpath%20d%3D%22M26.343%2C36.875v-5.041l9.583-8.669v-0.188l-9.583-8.668V9.267l12.868%2C11.872v3.863L26.343%2C36.875z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2253.75%22%20y1%3D%2239.353%22%20x2%3D%2286.375%22%20y2%3D%2239.353%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2251.711%22%20y1%3D%2230.534%22%20x2%3D%2284.336%22%20y2%3D%2230.534%22%2F%3E%0D%0A%3Crect%20x%3D%228.932%22%20y%3D%227.334%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.097%22%20height%3D%2224.952%22%2F%3E%0D%0A%3Crect%20x%3D%2218.942%22%20y%3D%2215.424%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.096%22%20height%3D%2224.953%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%229.25%22%20x2%3D%2239.75%22%20y2%3D%229.25%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2218.167%22%20x2%3D%2239.75%22%20y2%3D%2218.167%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2227.083%22%20x2%3D%2239.75%22%20y2%3D%2227.083%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2236%22%20x2%3D%2229.809%22%20y2%3D%2236%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22E%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20fill%3D%22%23202F65%22%20d%3D%22M32.48%2C25.614H19.64l-4.933%2C9.826l17.746%2C0.037l-6.173%2C5.358L8.167%2C40.912L16.29%2C6.055h22.974l-5.734%2C5.354%0D%0A%09%09l-13.306-0.027l0.672%2C8.797h12.841L32.48%2C25.614z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><div>
|
||||
<span>using</span> <span>Wasmtime;</span>
|
||||
</div><div>
|
||||
<span>using</span> <span>var</span> <span>engine =</span> <span>new</span> <span>Engine</span><span>()</span><span>;</span>
|
||||
</div><div>
|
||||
<span>using</span> <span>var</span> <span>module = Module.</span><span>FromText</span><span>(</span>
|
||||
</div><div>
|
||||
<span>engine,</span>
|
||||
</div><div>
|
||||
<span>"hello"</span><span>,</span>
|
||||
</div><div>
|
||||
<span>"(module (func $hello (import \"\" \"hello\")) (func (export \"run\") (call $hello)))"</span>
|
||||
</div><div>
|
||||
<span>)</span><span>;</span>
|
||||
</div><div>
|
||||
<span>using</span> <span>var</span> <span>linker =</span> <span>new</span> <span>Linker</span><span>(</span><span>engine</span><span>)</span><span>;</span>
|
||||
</div><div>
|
||||
<span>using</span> <span>var</span> <span>store =</span> <span>new</span> <span>Store</span><span>(</span><span>engine</span><span>)</span><span>;</span>
|
||||
</div><div>
|
||||
<span>linker.</span><span>Define</span><span>(</span>
|
||||
</div><div>
|
||||
<span>""</span><span>,</span>
|
||||
</div><div>
|
||||
<span>"hello"</span><span>,</span>
|
||||
</div><div>
|
||||
<span>Function.</span><span>FromCallback</span><span>(</span><span>store,</span> <span>()</span> <span>=</span><span>></span> <span>Console.</span><span>WriteLine</span><span>(</span><span>"Hello from C#!"</span><span>))</span>
|
||||
</div><div>
|
||||
<span>)</span><span>;</span>
|
||||
</div><div>
|
||||
<span>var</span> <span>instance = linker.</span><span>Instantiate</span><span>(</span><span>store, module</span><span>)</span><span>;</span>
|
||||
</div><div>
|
||||
<span>var</span> <span>run = instance.</span><span>GetAction</span><span>(</span><span>"run"</span><span>)</span><span>!;</span>
|
||||
</div><div>
|
||||
<span>run</span><span>()</span><span>;</span>
|
||||
</div><div>
|
||||
using Wasmtime; using var engine = new Engine(); using var module = Module.FromText( engine, "hello", "(module (func $hello (import \"\" \"hello\")) (func (export \"run\") (call $hello)))" ); using var linker = new Linker(engine); using var store = new Store(engine); linker.Define( "", "hello", Function.FromCallback(store, () => Console.WriteLine("Hello from C#!")) ); var instance = linker.Instantiate(store, module); var run = instance.GetAction("run")!; run();
|
||||
</div><pre>using Wasmtime;
|
||||
|
||||
using var engine = new Engine();
|
||||
|
||||
using var module = Module.FromText(
|
||||
engine,
|
||||
"hello",
|
||||
"(module (func $hello (import \"\" \"hello\")) (func (export \"run\") (call $hello)))"
|
||||
);
|
||||
|
||||
using var linker = new Linker(engine);
|
||||
using var store = new Store(engine);
|
||||
|
||||
linker.Define(
|
||||
"",
|
||||
"hello",
|
||||
Function.FromCallback(store, () => Console.WriteLine("Hello from C#!"))
|
||||
);
|
||||
|
||||
var instance = linker.Instantiate(store, module);
|
||||
var run = instance.GetAction("run")!;
|
||||
run();</pre><p>
|
||||
The critical thing to note in this example is that our WebAssembly module uses a reference to <code>$hello</code>. Since our C# application manages our WebAssembly context, we can create a definition for <code>$hello</code> using the method <code>Function.FromCallback</code>. In C#, we ask the linker, which now contains both our <code>.wat</code> definition and our new implementation of <code>$hello</code> for the <code>run</code> method. All that’s left is to invoke our <code>Action</code> and see the results.
|
||||
</p><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20d%3D%22M19.436%2C36.875L6.568%2C25.002v-3.863L19.436%2C9.267v5.041l-9.583%2C8.668v0.188l9.583%2C8.669V36.875z%22%2F%3E%0D%0A%09%3Cpath%20d%3D%22M26.343%2C36.875v-5.041l9.583-8.669v-0.188l-9.583-8.668V9.267l12.868%2C11.872v3.863L26.343%2C36.875z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2253.75%22%20y1%3D%2239.353%22%20x2%3D%2286.375%22%20y2%3D%2239.353%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2251.711%22%20y1%3D%2230.534%22%20x2%3D%2284.336%22%20y2%3D%2230.534%22%2F%3E%0D%0A%3Crect%20x%3D%228.932%22%20y%3D%227.334%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.097%22%20height%3D%2224.952%22%2F%3E%0D%0A%3Crect%20x%3D%2218.942%22%20y%3D%2215.424%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.096%22%20height%3D%2224.953%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%229.25%22%20x2%3D%2239.75%22%20y2%3D%229.25%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2218.167%22%20x2%3D%2239.75%22%20y2%3D%2218.167%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2227.083%22%20x2%3D%2239.75%22%20y2%3D%2227.083%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2236%22%20x2%3D%2229.809%22%20y2%3D%2236%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22E%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20fill%3D%22%23202F65%22%20d%3D%22M32.48%2C25.614H19.64l-4.933%2C9.826l17.746%2C0.037l-6.173%2C5.358L8.167%2C40.912L16.29%2C6.055h22.974l-5.734%2C5.354%0D%0A%09%09l-13.306-0.027l0.672%2C8.797h12.841L32.48%2C25.614z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><div>
|
||||
<span>Hello from C</span><span>#!</span>
|
||||
</div><div>
|
||||
Hello from C#!
|
||||
</div><pre>Hello from C#!</pre><p>
|
||||
While it may seem trivial, we’ll be able to run and consume WASM in our .NET applications. That opens up possibilities for solving problems using solutions from other ecosystems.
|
||||
</p><h2>
|
||||
The current reality of WASM on the server
|
||||
</h2><p>
|
||||
While this post has been relatively optimistic about the prospect of WASM, there are limitations to the technology that folks should be aware of, and they will vary based on the WASM host.
|
||||
</p><p>
|
||||
The first significant limitation is threading. So far, with my testing, Wasmtime only has access to a single thread for execution. Thread limits are not entirely a deal-breaker, but you’ll need to rewrite some of your solutions to reduce the need for threading. This limitation becomes apparent when using an API that involves <code>Thread</code> or <code>Task.Delay</code>, which will halt your running process or terminate it catastrophically. However, threading may not be an issue in the near future as <a href="https://github.com/dotnet/runtime/issues/68162">.NET adds multi-threaded support to WASM</a>. Wasmtime also enables multiple threads with an experimental flag, but your technology stack will need to take advantage of that feature.
|
||||
</p><p>
|
||||
The .NET runtime targeting WASM is the same as Blazor WASM, which is to say it has limits. APIs will not work based on a lack of support from the current WASM host. Missing APIs could limit your ability to solve specific problems. The APIs are still in active development and could change based on features added to Wasmtime and other WASI runtimes.
|
||||
</p><p>
|
||||
Another issue I’ve found is there is currently no outward socket support. Lack of socket support limits a WASM application’s ability to communicate with dependencies like a database or web service. <a href="https://github.com/WebAssembly/wasi-sockets">Discussions of a Socket specification</a> will resolve this issue, allowing for more robust WASM apps. In addition, this should enable .NET developers to use data access tools like Entity Framework Core or Dapper with few issues.
|
||||
</p><p>
|
||||
Tooling is another concern; vendors must catch up to the experiences .NET developers have gotten used to developing .NET applications. Luckily for developers, Wasmruntime uses standard debugging tools like LLDB and GDB, making integrating them into existing tools much more manageable.
|
||||
</p><p>
|
||||
Infrastructure will play an essential role in how you build your WASM solutions. The strength of WASM comes from the ability to start and dispose of executing modules quickly in the vein of “microservices” or “functions”. However, a different architectural approach will require you to rethink your existing applications’ orchestration and units of work. In addition, you and your team may not have the bandwidth to deal with the technical and conceptual challenges simultaneously.
|
||||
</p><p>
|
||||
How does a modern WASM-powered application look when deployed? Some folks think it looks like functions as a service (FaaS), while others see it as a replacement for containers in a Kubernetes cluster, and both are perfectly valid. Luckily, many new WASM hosts are springing up along with reliable cloud providers AWS, Microsoft Azure, and Cloudflare, adding options to their ever-expanding list of services.
|
||||
</p><h2>
|
||||
Imagining the future of WASM on the server
|
||||
</h2><p>
|
||||
We’ve reached the speculative part of the post, where we get to imagine the future of WASM and how it may transform your development experience, from writing code to deploying a solution. So let’s start with the process of development.
|
||||
</p><p>
|
||||
If you’ve developed any Blazor WASM application, you’ll likely realize that little feels different while writing code. After all, it’s C#! The debugging experience may be slightly different, as you now deal with a completely different host and runtime, specifically the web browser. However, writing solutions to target WASM will feel very similar to Blazor WASM, and I expect the experience to improve as tooling catches up. Switching to WASM apps will be easier for most developers than switching to a new programming paradigm.
|
||||
</p><p>
|
||||
The frameworks targeting WASM will likely change, as you will want to reconsider how and when functions are executing. For example, <a href="https://www.fermyon.com/blog/webassembly-for-dotnet-developers-spin-sdk-intro">Fermyon released a .NET Spin SDK</a> specifically for their hosting platform that targets WASM. Let’s look at how you might write a function given their SDK.
|
||||
</p><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20d%3D%22M19.436%2C36.875L6.568%2C25.002v-3.863L19.436%2C9.267v5.041l-9.583%2C8.668v0.188l9.583%2C8.669V36.875z%22%2F%3E%0D%0A%09%3Cpath%20d%3D%22M26.343%2C36.875v-5.041l9.583-8.669v-0.188l-9.583-8.668V9.267l12.868%2C11.872v3.863L26.343%2C36.875z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2253.75%22%20y1%3D%2239.353%22%20x2%3D%2286.375%22%20y2%3D%2239.353%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%2251.711%22%20y1%3D%2230.534%22%20x2%3D%2284.336%22%20y2%3D%2230.534%22%2F%3E%0D%0A%3Crect%20x%3D%228.932%22%20y%3D%227.334%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.097%22%20height%3D%2224.952%22%2F%3E%0D%0A%3Crect%20x%3D%2218.942%22%20y%3D%2215.424%22%20fill%3D%22%23FFFFFF%22%20stroke%3D%22%23000000%22%20stroke-width%3D%223%22%20stroke-miterlimit%3D%2210%22%20width%3D%2221.096%22%20height%3D%2224.953%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22Ebene_2%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%229.25%22%20x2%3D%2239.75%22%20y2%3D%229.25%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2218.167%22%20x2%3D%2239.75%22%20y2%3D%2218.167%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2227.083%22%20x2%3D%2239.75%22%20y2%3D%2227.083%22%2F%3E%0D%0A%3Cline%20fill%3D%22none%22%20stroke%3D%22%23000000%22%20stroke-width%3D%224%22%20stroke-miterlimit%3D%2210%22%20x1%3D%227.125%22%20y1%3D%2236%22%20x2%3D%2229.809%22%20y2%3D%2236%22%2F%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><img src="data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C!DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%20version%3D%221.1%22%20id%3D%22E%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20x%3D%220px%22%20y%3D%220px%22%0D%0A%09%20width%3D%2246px%22%20height%3D%2246px%22%20viewBox%3D%220%200%2046%2046%22%20enable-background%3D%22new%200%200%2046%2046%22%20xml%3Aspace%3D%22preserve%22%3E%0D%0A%3Cg%3E%0D%0A%09%3Cpath%20fill%3D%22%23202F65%22%20d%3D%22M32.48%2C25.614H19.64l-4.933%2C9.826l17.746%2C0.037l-6.173%2C5.358L8.167%2C40.912L16.29%2C6.055h22.974l-5.734%2C5.354%0D%0A%09%09l-13.306-0.027l0.672%2C8.797h12.841L32.48%2C25.614z%22%2F%3E%0D%0A%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E%0D%0A"/><div>
|
||||
<span>using</span> <span>System.Net;</span>
|
||||
</div><div>
|
||||
<span>using</span> <span>Fermyon.Spin.Sdk;</span>
|
||||
</div><div>
|
||||
<span>namespace</span> <span>Microservice;</span>
|
||||
</div><div>
|
||||
<span>public</span> <span>static</span> <span>class</span> <span>Handler</span>
|
||||
</div><div>
|
||||
<span>{</span>
|
||||
</div><div>
|
||||
<span>[</span><span>HttpHandler</span><span>]</span>
|
||||
</div><div>
|
||||
<span>public</span> <span>static</span> <span>HttpResponse</span> <span>HandleHttpRequest</span><span>(</span><span>HttpRequest request</span><span>)</span>
|
||||
</div><div>
|
||||
<span>{</span>
|
||||
</div><div>
|
||||
<span>return</span> <span>new</span> <span>HttpResponse</span>
|
||||
</div><div>
|
||||
<span>{</span>
|
||||
</div><div>
|
||||
<span>StatusCode = HttpStatusCode.</span><span>OK</span><span>,</span>
|
||||
</div><div>
|
||||
<span>BodyAsString =</span> <span>"Hello from .NET"</span><span>,</span>
|
||||
</div><div>
|
||||
<span>}</span><span>;</span>
|
||||
</div><div>
|
||||
<span>}</span>
|
||||
</div><div>
|
||||
<span>}</span>
|
||||
</div><div>
|
||||
using System.Net; using Fermyon.Spin.Sdk; namespace Microservice; public static class Handler { [HttpHandler] public static HttpResponse HandleHttpRequest(HttpRequest request) { return new HttpResponse { StatusCode = HttpStatusCode.OK, BodyAsString = "Hello from .NET", }; } }
|
||||
</div><pre>using System.Net;
|
||||
using Fermyon.Spin.Sdk;
|
||||
|
||||
namespace Microservice;
|
||||
|
||||
public static class Handler
|
||||
{
|
||||
[HttpHandler]
|
||||
public static HttpResponse HandleHttpRequest(HttpRequest request)
|
||||
{
|
||||
return new HttpResponse
|
||||
{
|
||||
StatusCode = HttpStatusCode.OK,
|
||||
BodyAsString = "Hello from .NET",
|
||||
};
|
||||
}
|
||||
}</pre><p>
|
||||
This example begs questions. For example, how do you transition from ASP.NET Core Minimal Endpoints or ASP.NET Core MVC to this new approach? Does ASP.NET Core need to rethink how to compile existing applications into deployable units? I expect a lot of conversations and strategies to emerge as developers adopt WASM.
|
||||
</p><p>
|
||||
We could see WASM replace most instances of containerization, especially when WASM can package both runtimes and the applications that depend on them in one portable package. Advantages you could hope to see: Reduced CPU and memory usage, decreased cold-boot times, reduced hosting bills, and more significant economies of scale.
|
||||
</p><p>
|
||||
These <code>.wasm</code> files can also be considerably smaller as they don’t have the same need for layers found in images built on entire operating systems. The portability and efficiency of WASM hosts will lead to more hosting options from vendors, new and old.
|
||||
</p><p>
|
||||
Finally, the most exciting prospect of WASM on the server is the fulfillment of an agnostic cloud environment, where you, as the developer, no longer have to worry about regions. WASM can be deployed simultaneously to many global regions beyond cloud vendors’ limited and congested regions. WASM can start closest to your users and provide them with the fastest experience possible regardless of their country of origin. This democratization of user experience is most exciting for folks delivering global-scale applications.
|
||||
</p><h2>
|
||||
Conclusion
|
||||
</h2><p>
|
||||
In this post, we delved into WebAssembly and the efforts to run it on the server using Wasmtime. While it’s still relatively new, the promise of WASM on the server is exciting, and .NET is at the forefront of giving developers more hosting options. As tooling and development experience improves, so will the solutions developers deliver to customers. As you’ve seen, the experience even now is good.
|
||||
</p><p>
|
||||
There are limitations with WASM, but like all technologies, the community will push solutions forward, and progress will occur. It’s still important to be careful when choosing cutting-edge technologies as solutions, as you’ll likely be some of the first and only folks experiencing those issues.
|
||||
</p><p>
|
||||
That said, I’m cautiously optimistic about the future of WASM, and I hope you found something in this article that has sparked your curiosity to explore the topic further.
|
||||
</p></div>
|
||||
|
|
@ -0,0 +1,876 @@
|
|||
<div><div>Productivity is one of my pet topics, because it's always dogged me a bit, especially early in my career. I'd pull long days and nights and then realize I only actually worked (as in, typing in code, debugging stuff, and thinking about problems and their solutions) maybe 20% of the time. Upon talking to coworkers, this seemed to be <i>normal, </i>a part of the expected friction costs incurred working in an office environment. Meetings, shooting the shit with coworkers, lunch, email, and, er, <i>stuff</i>.<br/>
|
||||
</div><img src="http://upload.wikimedia.org/wikipedia/en/9/93/Netscape_Navigator.png"/>This is what I mean by 'stuff' in 1996<div>
|
||||
Eventually working around high-productivity professionals like John Carmack made me realize that if you want to excel, then you have to work hard and focus the whole time.</div><img src="http://img2u.info/ckgni/i/c48b1ced.jpg"/>John Carmack Productivity Measurement Tool ca. 1998<div>
|
||||
I remember Carmack talking about productivity measurement. While working he would play a CD, and if he was not being productive, he'd pause the CD player. This meant any time someone came into his office to ask him a question or he checked email he'd pause the CD player. He'd then measure his output for the day by how many times he played the CD (or something like that -- maybe it was how far he got down into his CD stack). I distinctly remember him saying "So if I get up to go to the bathroom, I pause the player". <br/>
|
||||
<br/>
|
||||
You know what's pretty hardcore? Thinking that <i>going to the bathroom</i> is essentially the same as fucking off.</div><div>
|
||||
(Sidenote: my memory is hazy on this since it was long ago -- CD PLAYERS yo).<br/>
|
||||
<br/></div><div>
|
||||
That level of work ethic and focus is rare, particularly with my generation of programmers who were raised with the vile "work smart, not hard" mantra, coupled with the sense that we were somehow significantly brighter than most of our peers. Combine those two notions and add the <a href="http://www.urbandictionary.com/define.php?term=Scotty%20Principle">Scotty Principle</a> and you get....me.</div><img src="http://cdn.memegenerator.net/instances/400x/36064437.jpg"/><div>If my coworkers were grinding through stuff that took them 20 hours, I'd be all "I work <i>smart</i>, not <i>hard</i>" with accompanying snarky eye roll, and I'd assume I could bust through an equivalent work load in four hours and still look like a goddamn superhero.</div><div>
|
||||
And sometimes I could, so, hey, validation! </div><div>
|
||||
And you can skate by like this (God knows I did) for a long time before a couple things eventually rear their heads and bite you in your entitled face.</div><h4>
|
||||
Productivity Deficit: Your Attitude Writing Checks Your Work Ethic Can't Cash</h4><div>
|
||||
An overinflated sense of your own abilities creates a constant state of production deficit, because you assume that you can make it up with a burst of brilliance and/or crunch. </div><div>
|
||||
But there is no countering surplus to offset the deficit. The only way surpluses show up is when you finish a (presumably) hard task much faster than you anticipated. But instead of banking the surplus (i.e. moving on immediately to your next task), you spend it relaxing and screwing off because, whew, you just earned a small vacation by busting shit out in an hour that you thought would take all day. Hey, what's on Facebook and Twitter right now? Do I have any new mail? No? I wonder if anyone has tweeted something recently since the last time I checked...what about Reddit? Oh, an inspirational/funny/cool YouTube video, it's only eight minutes long, let me watch it now! (Eight minutes later) Sweet...what's up Twitter? Oh man, I have to make a snarky response, but first I have to Google for that XKCD comic that totally makes my point for me...</div><div>
|
||||
And before you know it, the day is done, and you're still feeling good because you finished in one hour what should have taken all day, so all good, right?<br/>
|
||||
</div><h4>
|
||||
Trap of the Easy Task</h4><div>
|
||||
And yeah, it's often all good, but when operating at a slight deficit things can go pear shaped quickly when you accidentally spring the trap of the easy task. Tasks so trivial that they barely register as work. Update an SDK? Hour, tops. Implement this feature that I've already done on every other platform? Hour, tops. This is all mindless grunt work that anyone could do, it's just clicking through dialog boxes, maybe changing a few lines of source, rebuilding, whatever.</div><div>
|
||||
In other words, an easy task like this is <i>so</i> easy that it's a constant time cost for everyone irrespective of ability, so there's no opportunity nor need for crazy overestimation since <i>what could possibly go wrong?</i></div><div>
|
||||
Well, as with so many things, it's the unexpected things that screw you the hardest. The things that <i>should</i> be trivial end up taking days because, shit, that SDK install required a new compiler install, and the new compiler install failed and I can't uninstall it, and now I have to fucking WIPE MY WHOLE GODDAMN HARD DRIVE TO RECOVER. Or you need to implement something that was trivial on nine other platforms, and it should be 30 minutes on platform number ten, except it lacks a base feature that is present on everything else, the compiler is dying with an "Internal compiler failure 2033", the API call you need to use hangs the dev console immediately and, oh, the dev support site you use to get release notes is totally down for the foreseeable future.</div><div>
|
||||
So what should have taken less than an hour took a week.</div><div>
|
||||
It's like having a perfect monetary budget that assumes no crazy "one time" only bills, except life is full of crazy one time only bills and the only way you can keep those under control is by giving yourself a budgetary capacitor to dampen the fluctuations.</div><div>
|
||||
And now you're defensive about losing a week to something stupid because you budgeted an hour for it and waited until the last second to do it and now the schedule has gone to hell, but it's not <i>your</i> fault, because it could have happened to anyone! But if you had banked your surplus hours before and/or worked at closer to your theoretical peak effectiveness then this type of thing would get absorbed in the wash.</div><div>
|
||||
And now you live in a state of mild panic because you're way smarter than all this but you're never actually getting things done on time.</div><h4>
|
||||
Identity Recalibration Crisis</h4><div>
|
||||
Which leads to a potential identity recalibration crisis upon landing at a company with high performers that work hard <i>and</i> smart. And that will happen if you're good. Now you're no longer at the top of the curve. In fact, shit, you're in the middle or <i>bottom</i> of the curve, a situation your brain probably never considered as an option.<br/>
|
||||
<br/>
|
||||
</div><img src="http://img2u.info/ckgni/i/539249a2.jpg"/>This dude was my wake up call with respect to my<br/>
|
||||
over inflated sense of skill<div>
|
||||
I went through this when I went to id software. I had rationalized that John Carmack's success was just a factor of timing and luck since, hell, he was my age, and I was pretty smart, so what else could it be? Upon my arrival I had a crash course in humility, because he was <i>way</i> smarter than me and <i>way</i> more productive as well.<br/>
|
||||
<br/>
|
||||
This took a while to sink in, because until then I was used to believing I was one of the better programmers at a company. And then working with Carmack I realized he was not just a little better, he was <i>orders of magnitude </i>better. And I couldn't dismiss it with a "But I write a lot of code" hand wave, because he also wrote like 80% of the Quake code base.<br/>
|
||||
<br/></div><div>
|
||||
Sometimes it takes a while for this to sink in. Sometimes it doesn't sink in at all, and you're let go because you're not very productive compared to your new team of high performers. Sometimes it sinks in, and you panic and get depressed because your self-image has always been that of being the strongest swimmer in the school, and right now you're just trying not to drown, much less keep up with everyone else.</div><div>
|
||||
But ideally you shrug off the old counter productive mentality and habits and emerge as another one of the high functioning team members, but that can take a lot of work, particularly if you have to get over years of giving it twenty percent.<br/>
|
||||
</div><h4>
|
||||
Killing the Underachiever</h4><div>
|
||||
If my pithy advice were little more than "Be more like John Carmack" then I can imagine a lot of readers throwing up their hands and saying "Well, fuck it, I'm a lost cause, because that's not going to happen." But what I can do is relate some of the things that helped me kill off some of my underachieving habits. The point isn't to become a superstar, it's to become better, since that's always the first step.<br/>
|
||||
<br/>
|
||||
I don't believe in mechanical solutions ("Turn off the internet", "Listen to music", and stuff like that) because I don't think they address the core issues, which are psychological. Instead I found that I had to do the following.<br/>
|
||||
<br/>
|
||||
</div><ol><li><b>Develop self-awareness</b>. It took working with John Carmack and other high productivity programmers and <i>admitting that they were way more productive than me</i> before I really understood how much more productive I could be. In other words, if you don't admit it's something worth improving, then obviously you're not going to search for a solution, and if that's the case, you should go <a href="http://www.reddit.com/">here </a>or <a href="https://www.facebook.com/">here</a>.</li><li><b>Give a shit</b>. Originally I called this "develop a sense of urgency", but really it's just about caring about getting your work done. It doesn't even matter what you specifically care about! It can be your professional image, your product, your team, your customers, whatever. You just have to care about something that will drive you to getting things done, because if you don't, apathy will occupy that void.</li><li><b>Minimize uncertainty</b>. In another blog article, <a href="http://bookofhook.blogspot.com/2012/09/productivity-vs-uncertainty-apathy.html">Productivity vs. Uncertainty and Apathy</a>, I talk about how poorly defined goals can lead to poor productivity. If it's unclear what you need to get done <i>today</i>, then there's a reasonable chance you won't actually <i>do </i>anything today.</li><li><b>Commit to getting something done every day.</b> When you show up in the morning have a well defined set of things to finish that day. Stay as late as you have to in order to finish. By committing to finishing that task other distractions will naturally fall by the wayside. For example, I have jiu-jitsu training at 7pm. If I screw off too much during the day, I don't get to train. Also, by committing to a task, you avoid "being busy" instead of "getting work done", <a href="http://bookofhook.blogspot.com/2013/01/being-busy-isnt-same-as-getting-work.html">they're not the same thing</a>.</li><li><b>Never say "I'll finish it up tomorrow" or "I'll make up for it by coming in early/staying late/working the weekend". </b> This is an easy trap to get into, where you keep incurring time debt until at some point you realize you're now three weeks behind on a task that should have taken two days. This is like racking up credit card bills assuming you can pay them off later. Which is fine, until "later" arrives and you've only accumulated more debt.</li><li><b>Do not overpromise to make up for poor productivity</b>. There's a tendency when we're falling behind to try to overcompensate with future promises. "When I'm done, it'll be AWESOME" or "I know I'm late, but I'm positive I'll be done by Monday". By doing those things we just build more debt we can't pay off, and that will eventually lead to a catastrophic melt down when the super final absolutely last deadline date shows up. Just get shit done, don't talk about how you're going to get shit done.</li><li><b>Have an objective productivity metric.</b> This is a mechanical thing, but it acts as a reasonable backstop. If you have changelogs you can reference, then it's easy to sit down on Friday and ask yourself "What have I done this week?" And if you know that it's possible for others to check on you, then it makes you take each day a lot more seriously. If you judge your value solely on output, not subjective things like "being smart", you will be more productive.</li><li><b>Accept that "the grind" is part of the job</b>. A friend of mine's father has a great quote: "Son, i don't wake up every day and go to a place called fun. I wake up and go to a place called <i>work</i>" You can't get irate or frustrated that the bulk of the day is typing in boring code and dealing with bugs and other people.</li></ol><div>
|
||||
I still screw off during the day. I am not a code grinding automaton. I read Facebook, chat with others, Tweet, shop on Amazon, get coffee, read forums, write blog posts like this one<i> </i>and I'm <i>totally fine with that</i>. </div><img src="http://cdn.memegenerator.net/instances/400x/36175351.jpg"/><div>
|
||||
Because I know I'm committed to getting things done as well. So even though I'm writing this right now at 3pm, if I have to work until 8pm to get my shit done and checked in, I'll do that, so that tomorrow is a fresh start, not a day of "Oh goddamit, let's try <i>again</i> to finish that task I started a week ago."</div><div>
|
||||
Once I developed that sense of daily urgency my productivity went up considerably. And if I'm really productive in the morning and afternoon and feel like I can go home at 4pm with a clean conscience, I do that too, so I don't burn out. </div><div>
|
||||
It's a push/pull between banking surplus and generating a deficit, just make sure you don't do just one or the other.</div><div>
|
||||
|
||||
|
||||
Posted <abbr title="2013-03-12T23:10:00.003Z">12th March 2013</abbr> by Unknown
|
||||
|
||||
</div><ol><li><div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
<a title="25th April 2014" href="http://bookofhook.blogspot.com/2014/04/this-advice-is-worthless.html">
|
||||
|
||||
|
||||
<div>Apr</div>
|
||||
<div>25</div>
|
||||
<div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<h1>
|
||||
|
||||
|
||||
<a href="http://bookofhook.blogspot.com/2014/04/this-advice-is-worthless.html" rel="bookmark">
|
||||
|
||||
|
||||
|
||||
This Advice Is Worthless</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
<div>I was originally going to write this blog about a different topic, one of the endless online discussions about "How to get better at this thing you suck at". Then I realized it was pointless.<br/>
|
||||
<br/>
|
||||
Many "How to make your life/code/job/hobby better" blogs are very well written, insightful, and even inspirational. They provide awareness into our failings and weaknesses and hint at ways in which we can improve. They give us strategies and high level conceptual ideas on how to approach problems.<br/>
|
||||
<br/>
|
||||
The issue is that it's all mostly pointless. Finding the best plan doesn't mean we'll execute it, because very often the problem is that we suck at execution. The very things we're trying to fix are often the very things that prevent us from fixing them.<br/>
|
||||
<br/>
|
||||
Or, put another way, it's a rare situation where someone doesn't know what to do, is told what to do, then goes off and just does it.<br/>
|
||||
<br/>
|
||||
Have a nice day.<br/>
|
||||
<br/></div>
|
||||
<div>
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
Posted <abbr title="2014-04-25T15:39:00.005Z">25th April 2014</abbr> by Unknown
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div></li><li><div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
<a title="12th March 2013" href="http://bookofhook.blogspot.com/2013/03/smart-guy-productivity-pitfalls.html">
|
||||
|
||||
|
||||
<div>Mar</div>
|
||||
<div>12</div>
|
||||
<div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<h1>
|
||||
|
||||
|
||||
<a href="http://bookofhook.blogspot.com/2013/03/smart-guy-productivity-pitfalls.html" rel="bookmark">
|
||||
|
||||
|
||||
|
||||
Smart Guy Productivity Pitfalls</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
<div>Productivity is one of my pet topics, because it's always dogged me a bit, especially early in my career. I'd pull long days and nights and then realize I only actually worked (as in, typing in code, debugging stuff, and thinking about problems and their solutions) maybe 20% of the time. Upon talking to coworkers, this seemed to be <i>normal, </i>a part of the expected friction costs incurred working in an office environment. Meetings, shooting the shit with coworkers, lunch, email, and, er, <i>stuff</i>.<br/>
|
||||
<table><tbody>
|
||||
</tbody></table></div></div><img src="http://upload.wikimedia.org/wikipedia/en/9/93/Netscape_Navigator.png"/><div><table><tbody>
|
||||
<tr><td>This is what I mean by 'stuff' in 1996</td></tr>
|
||||
</tbody></table>
|
||||
<br/>
|
||||
<div>
|
||||
<div>
|
||||
Eventually working around high-productivity professionals like John Carmack made me realize that if you want to excel, then you have to work hard and focus the whole time.</div>
|
||||
|
||||
<div>
|
||||
<table><tbody>
|
||||
</tbody></table></div></div></div><img src="http://img2u.info/ckgni/i/c48b1ced.jpg"/><div><div><table><tbody>
|
||||
<tr><td>John Carmack Productivity Measurement Tool ca. 1998</td></tr>
|
||||
</tbody></table>
|
||||
I remember Carmack talking about productivity measurement. While working he would play a CD, and if he was not being productive, he'd pause the CD player. This meant any time someone came into his office to ask him a question or he checked email he'd pause the CD player. He'd then measure his output for the day by how many times he played the CD (or something like that -- maybe it was how far he got down into his CD stack). I distinctly remember him saying "So if I get up to go to the bathroom, I pause the player". <br/>
|
||||
<br/>
|
||||
You know what's pretty hardcore? Thinking that <i>going to the bathroom</i> is essentially the same as fucking off.</div>
|
||||
|
||||
<div>
|
||||
(Sidenote: my memory is hazy on this since it was long ago -- CD PLAYERS yo).<br/>
|
||||
<br/></div>
|
||||
<div>
|
||||
That level of work ethic and focus is rare, particularly with my generation of programmers who were raised with the vile "work smart, not hard" mantra, coupled with the sense that we were somehow significantly brighter than most of our peers. Combine those two notions and add the <a href="http://www.urbandictionary.com/define.php?term=Scotty%20Principle">Scotty Principle</a> and you get....me.</div>
|
||||
|
||||
<div>
|
||||
</div></div><img src="http://cdn.memegenerator.net/instances/400x/36064437.jpg"/><div><div>If my coworkers were grinding through stuff that took them 20 hours, I'd be all "I work <i>smart</i>, not <i>hard</i>" with accompanying snarky eye roll, and I'd assume I could bust through an equivalent work load in four hours and still look like a goddamn superhero.</div>
|
||||
|
||||
<div>
|
||||
And sometimes I could, so, hey, validation! </div>
|
||||
|
||||
<div>
|
||||
And you can skate by like this (God knows I did) for a long time before a couple things eventually rear their heads and bite you in your entitled face.</div>
|
||||
<div>
|
||||
<h4>
|
||||
Productivity Deficit: Your Attitude Writing Checks Your Work Ethic Can't Cash</h4>
|
||||
</div>
|
||||
<div>
|
||||
An overinflated sense of your own abilities creates a constant state of production deficit, because you assume that you can make it up with a burst of brilliance and/or crunch. </div>
|
||||
|
||||
<div>
|
||||
But there is no countering surplus to offset the deficit. The only way surpluses show up is when you finish a (presumably) hard task much faster than you anticipated. But instead of banking the surplus (i.e. moving on immediately to your next task), you spend it relaxing and screwing off because, whew, you just earned a small vacation by busting shit out in an hour that you thought would take all day. Hey, what's on Facebook and Twitter right now? Do I have any new mail? No? I wonder if anyone has tweeted something recently since the last time I checked...what about Reddit? Oh, an inspirational/funny/cool YouTube video, it's only eight minutes long, let me watch it now! (Eight minutes later) Sweet...what's up Twitter? Oh man, I have to make a snarky response, but first I have to Google for that XKCD comic that totally makes my point for me...</div>
|
||||
|
||||
<div>
|
||||
And before you know it, the day is done, and you're still feeling good because you finished in one hour what should have taken all day, so all good, right?<br/>
|
||||
<h4>
|
||||
Trap of the Easy Task</h4>
|
||||
</div>
|
||||
<div>
|
||||
And yeah, it's often all good, but when operating at a slight deficit things can go pear shaped quickly when you accidentally spring the trap of the easy task. Tasks so trivial that they barely register as work. Update an SDK? Hour, tops. Implement this feature that I've already done on every other platform? Hour, tops. This is all mindless grunt work that anyone could do, it's just clicking through dialog boxes, maybe changing a few lines of source, rebuilding, whatever.</div>
|
||||
|
||||
<div>
|
||||
In other words, an easy task like this is <i>so</i> easy that it's a constant time cost for everyone irrespective of ability, so there's no opportunity nor need for crazy overestimation since <i>what could possibly go wrong?</i></div>
|
||||
|
||||
<div>
|
||||
Well, as with so many things, it's the unexpected things that screw you the hardest. The things that <i>should</i> be trivial end up taking days because, shit, that SDK install required a new compiler install, and the new compiler install failed and I can't uninstall it, and now I have to fucking WIPE MY WHOLE GODDAMN HARD DRIVE TO RECOVER. Or you need to implement something that was trivial on nine other platforms, and it should be 30 minutes on platform number ten, except it lacks a base feature that is present on everything else, the compiler is dying with an "Internal compiler failure 2033", the API call you need to use hangs the dev console immediately and, oh, the dev support site you use to get release notes is totally down for the foreseeable future.</div>
|
||||
|
||||
<div>
|
||||
So what should have taken less than an hour took a week.</div>
|
||||
|
||||
<div>
|
||||
It's like having a perfect monetary budget that assumes no crazy "one time" only bills, except life is full of crazy one time only bills and the only way you can keep those under control is by giving yourself a budgetary capacitor to dampen the fluctuations.</div>
|
||||
|
||||
<div>
|
||||
And now you're defensive about losing a week to something stupid because you budgeted an hour for it and waited until the last second to do it and now the schedule has gone to hell, but it's not <i>your</i> fault, because it could have happened to anyone! But if you had banked your surplus hours before and/or worked at closer to your theoretical peak effectiveness then this type of thing would get absorbed in the wash.</div>
|
||||
|
||||
<div>
|
||||
And now you live in a state of mild panic because you're way smarter than all this but you're never actually getting things done on time.</div>
|
||||
<div>
|
||||
<h4>
|
||||
Identity Recalibration Crisis</h4>
|
||||
</div>
|
||||
<div>
|
||||
Which leads to a potential identity recalibration crisis upon landing at a company with high performers that work hard <i>and</i> smart. And that will happen if you're good. Now you're no longer at the top of the curve. In fact, shit, you're in the middle or <i>bottom</i> of the curve, a situation your brain probably never considered as an option.<br/>
|
||||
<br/>
|
||||
<table><tbody>
|
||||
</tbody></table></div></div><img src="http://img2u.info/ckgni/i/539249a2.jpg"/><div><div><table><tbody>
|
||||
<tr><td>This dude was my wake up call with respect to my<br/>
|
||||
over inflated sense of skill</td></tr>
|
||||
</tbody></table>
|
||||
I went through this when I went to id software. I had rationalized that John Carmack's success was just a factor of timing and luck since, hell, he was my age, and I was pretty smart, so what else could it be? Upon my arrival I had a crash course in humility, because he was <i>way</i> smarter than me and <i>way</i> more productive as well.<br/>
|
||||
<br/>
|
||||
This took a while to sink in, because until then I was used to believing I was one of the better programmers at a company. And then working with Carmack I realized he was not just a little better, he was <i>orders of magnitude </i>better. And I couldn't dismiss it with a "But I write a lot of code" hand wave, because he also wrote like 80% of the Quake code base.<br/>
|
||||
<br/></div>
|
||||
<div>
|
||||
Sometimes it takes a while for this to sink in. Sometimes it doesn't sink in at all, and you're let go because you're not very productive compared to your new team of high performers. Sometimes it sinks in, and you panic and get depressed because your self-image has always been that of being the strongest swimmer in the school, and right now you're just trying not to drown, much less keep up with everyone else.</div>
|
||||
|
||||
<div>
|
||||
But ideally you shrug off the old counter productive mentality and habits and emerge as another one of the high functioning team members, but that can take a lot of work, particularly if you have to get over years of giving it twenty percent.<br/>
|
||||
<h4>
|
||||
Killing the Underachiever</h4>
|
||||
If my pithy advice were little more than "Be more like John Carmack" then I can imagine a lot of readers throwing up their hands and saying "Well, fuck it, I'm a lost cause, because that's not going to happen." But what I can do is relate some of the things that helped me kill off some of my underachieving habits. The point isn't to become a superstar, it's to become better, since that's always the first step.<br/>
|
||||
<br/>
|
||||
I don't believe in mechanical solutions ("Turn off the internet", "Listen to music", and stuff like that) because I don't think they address the core issues, which are psychological. Instead I found that I had to do the following.<br/>
|
||||
<br/>
|
||||
</div></div><ol><li><b>Develop self-awareness</b>. It took working with John Carmack and other high productivity programmers and <i>admitting that they were way more productive than me</i> before I really understood how much more productive I could be. In other words, if you don't admit it's something worth improving, then obviously you're not going to search for a solution, and if that's the case, you should go <a href="http://www.reddit.com/">here </a>or <a href="https://www.facebook.com/">here</a>.</li><li><b>Give a shit</b>. Originally I called this "develop a sense of urgency", but really it's just about caring about getting your work done. It doesn't even matter what you specifically care about! It can be your professional image, your product, your team, your customers, whatever. You just have to care about something that will drive you to getting things done, because if you don't, apathy will occupy that void.</li><li><b>Minimize uncertainty</b>. In another blog article, <a href="http://bookofhook.blogspot.com/2012/09/productivity-vs-uncertainty-apathy.html">Productivity vs. Uncertainty and Apathy</a>, I talk about how poorly defined goals can lead to poor productivity. If it's unclear what you need to get done <i>today</i>, then there's a reasonable chance you won't actually <i>do </i>anything today.</li><li><b>Commit to getting something done every day.</b> When you show up in the morning have a well defined set of things to finish that day. Stay as late as you have to in order to finish. By committing to finishing that task other distractions will naturally fall by the wayside. For example, I have jiu-jitsu training at 7pm. If I screw off too much during the day, I don't get to train. Also, by committing to a task, you avoid "being busy" instead of "getting work done", <a href="http://bookofhook.blogspot.com/2013/01/being-busy-isnt-same-as-getting-work.html">they're not the same thing</a>.</li><li><b>Never say "I'll finish it up tomorrow" or "I'll make up for it by coming in early/staying late/working the weekend". </b> This is an easy trap to get into, where you keep incurring time debt until at some point you realize you're now three weeks behind on a task that should have taken two days. This is like racking up credit card bills assuming you can pay them off later. Which is fine, until "later" arrives and you've only accumulated more debt.</li><li><b>Do not overpromise to make up for poor productivity</b>. There's a tendency when we're falling behind to try to overcompensate with future promises. "When I'm done, it'll be AWESOME" or "I know I'm late, but I'm positive I'll be done by Monday". By doing those things we just build more debt we can't pay off, and that will eventually lead to a catastrophic melt down when the super final absolutely last deadline date shows up. Just get shit done, don't talk about how you're going to get shit done.</li><li><b>Have an objective productivity metric.</b> This is a mechanical thing, but it acts as a reasonable backstop. If you have changelogs you can reference, then it's easy to sit down on Friday and ask yourself "What have I done this week?" And if you know that it's possible for others to check on you, then it makes you take each day a lot more seriously. If you judge your value solely on output, not subjective things like "being smart", you will be more productive.</li><li><b>Accept that "the grind" is part of the job</b>. A friend of mine's father has a great quote: "Son, i don't wake up every day and go to a place called fun. I wake up and go to a place called <i>work</i>" You can't get irate or frustrated that the bulk of the day is typing in boring code and dealing with bugs and other people.</li></ol><div>
|
||||
<div>
|
||||
I still screw off during the day. I am not a code grinding automaton. I read Facebook, chat with others, Tweet, shop on Amazon, get coffee, read forums, write blog posts like this one<i> </i>and I'm <i>totally fine with that</i>. </div>
|
||||
<div>
|
||||
</div></div><img src="http://cdn.memegenerator.net/instances/400x/36175351.jpg"/><div><div><div><div>
|
||||
<div>
|
||||
<br/></div>
|
||||
<div>
|
||||
Because I know I'm committed to getting things done as well. So even though I'm writing this right now at 3pm, if I have to work until 8pm to get my shit done and checked in, I'll do that, so that tomorrow is a fresh start, not a day of "Oh goddamit, let's try <i>again</i> to finish that task I started a week ago."</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Once I developed that sense of daily urgency my productivity went up considerably. And if I'm really productive in the morning and afternoon and feel like I can go home at 4pm with a clean conscience, I do that too, so I don't burn out. </div>
|
||||
|
||||
<div>
|
||||
It's a push/pull between banking surplus and generating a deficit, just make sure you don't do just one or the other.</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
Posted <abbr title="2013-03-12T23:10:00.003Z">12th March 2013</abbr> by Unknown
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div></li><li><div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
<a title="20th February 2013" href="http://bookofhook.blogspot.com/2013/02/missing-features-are-better-than.html">
|
||||
|
||||
|
||||
<div>Feb</div>
|
||||
<div>20</div>
|
||||
<div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<h1>
|
||||
|
||||
|
||||
<a href="http://bookofhook.blogspot.com/2013/02/missing-features-are-better-than.html" rel="bookmark">
|
||||
|
||||
|
||||
|
||||
Missing Features Are Better Than Partial Features</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
<div>Telemetry was the first product I worked on where I interacted directly with customers. This interaction creates an interesting real time tension between working on big features with long development periods versus working on smaller features that can immediately benefit a small number of customers.<br/>
|
||||
|
||||
<div>
|
||||
Typically it starts with a reasonable request -- let's call it Feature X -- so we don't want to just say 'no'. But of course, we have to deal with bug reports, customer support, sales assistance, new platforms and previously promised or planned features, and all within the constraint of monthly releases. </div>
|
||||
|
||||
<div>
|
||||
As a result the choice boils down to "Do you want some of Feature X now, or all of Feature X much later?" In a perfect world we just do Feature X right and everyone wins, but in the real world of constrained bandwidth we have to be deliberate about scheduling work.</div>
|
||||
|
||||
<div>
|
||||
Very often Feature X can be implemented in a limited form and the customer is happy because, hey, what wasn't possible is now possible. And if we focus on just that customer's response then this is a net win, because that partial implementation may have taken only an hour whereas a proper, fully fleshed out implementation might have taken several days due to cascading side effects.</div>
|
||||
|
||||
<div>
|
||||
The problem arises when you look at this new feature in context of other customers. Now they see "Whoa, Feature X, cool!" or maybe just "Huh, didn't know about Feature X, I'll give it a try". The former camp has learned to work around the absence of Feature X, the latter camp didn't even know it existed and didn't care, but now that they're aware of it they want to use it.</div>
|
||||
|
||||
<div>
|
||||
And this is when the partial implementation absolutely turns out to be the wrong call. Because for that one customer who thinks "Those guys are awesome, they were super responsive to a feature request!" you now have multiple (potential) customers thinking "This feature is janky, what the hell?"</div>
|
||||
|
||||
<div>
|
||||
And they don't care about the reasons for it. They don't care that it wasn't even going to get implemented until a customer requested it, or the reasons that a full implementation isn't practical or maybe even possible, they're just kind of annoyed that Feature X is incomplete and/or buggy.</div>
|
||||
|
||||
<div>
|
||||
The lesson is this: if you're not going to do it right, then you have to really think hard about whether it's worth doing. Saying no to one customer is a lot better than explaining to all the others why Feature X isn't as good as they expect. And it's a tough thing to say, because it's easy to rationalize that a limited Feature X is better than no Feature X, but in my experience this is almost never the case.</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
Posted <abbr title="2013-02-20T19:44:00.004Z">20th February 2013</abbr> by Unknown
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div></li><li><div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
<a title="8th January 2013" href="http://bookofhook.blogspot.com/2013/01/being-busy-isnt-same-as-getting-work.html">
|
||||
|
||||
|
||||
<div>Jan</div>
|
||||
<div>8</div>
|
||||
<div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<h1>
|
||||
|
||||
|
||||
<a href="http://bookofhook.blogspot.com/2013/01/being-busy-isnt-same-as-getting-work.html" rel="bookmark">
|
||||
|
||||
|
||||
|
||||
Being Busy Isn't the Same As Getting Shit Done</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
<div>In software development it's easy to lose sight of the fact that we're supposed to be shipping software. We can show up to work, be legitimately busy for eight or nine hours, go home, and then realize that we didn't get any closer to shipping. <br/>
|
||||
<br/>
|
||||
<div>
|
||||
Many people are fine with this -- "Well, I 'did stuff', so hey, all good, right?" -- despite not getting any closer to shipping. Their day was consumed with necessary-but-not-shipping tasks. Things like installing software, replacing a hard drive, helping a coworker, setting up a dev kit, or debugging a feature you thought you were done with a year ago. This hidden but large frictional load can easily consume so much of our time that over a given period of time, <i>we never actually get closer to ship</i>. <br/>
|
||||
<br/>
|
||||
This is particularly true with mature products, where you now spend a lot of time on non-development tasks like support, sales, meetings, and training, and the 'being busy' part can become so dominant that you never get around to shipping a new version. This overhead is rarely factored into any schedule since coders are notoriously shitty at accounting for non-coding things. We think in best case terms like "That should take like a day" with the assumption "day" means "calendar day" but we never get a calendar day to do stuff, so "day" means "eight hours" which takes a week to get done.<br/>
|
||||
<br/>
|
||||
Assuming that crunching non-stop is off the table as an option, I've found that the biggest productivity boost I've had comes from asking myself "What do I need to get done today?" instead of "What will I work on today?". By creating a sense of urgency to finish a well defined task I now have to be cognizant of all the little interruptions that interfere with that goal through the day.<br/>
|
||||
<br/>
|
||||
So get shit done and stop just being busy. Or to quote David Wong (or Jesus, take your pick), <i>a tree is judged by its fruit</i>.<br/>
|
||||
<br/>
|
||||
<br/></div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
Posted <abbr title="2013-01-08T17:31:00.001Z">8th January 2013</abbr> by Unknown
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div></li><li><div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
<a title="11th September 2012" href="http://bookofhook.blogspot.com/2012/09/productivity-vs-uncertainty-apathy.html">
|
||||
|
||||
|
||||
<div>Sep</div>
|
||||
<div>11</div>
|
||||
<div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<h1>
|
||||
|
||||
|
||||
<a href="http://bookofhook.blogspot.com/2012/09/productivity-vs-uncertainty-apathy.html" rel="bookmark">
|
||||
|
||||
|
||||
|
||||
Productivity vs. Uncertainty & Apathy</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
<div>If you're not getting shit done, it may not be because you suck. Traditionally unproductive workers get nailed with labels like <i>undisciplined, lack of focus</i>, or <i>poor work ethic</i>, but in my experience these are not nearly as influential as I'd thought.<br/>
|
||||
<br/>
|
||||
Over the past two decades of coding, including stints in management/as a lead/and being self-directed with no immediate manager, I've come to the conclusion that the main issues that impact productivity are <i>uncertainty</i> and <i>apathy</i>.<br/>
|
||||
<br/>
|
||||
When uncertainty kicks me in the nuts, it's usually in one of several flavors: <b>high-level uncertainty</b>, <b>strategic uncertainty</b>, and <b>low-level uncertainty</b>.<br/>
|
||||
<br/>
|
||||
<table><tbody>
|
||||
</tbody></table></div></div><img src="http://img2u.info/ckgni/i/94fb1e59.jpg"/><div><table><tbody>
|
||||
<tr><td>"I feel like I should be doing something, but I have no idea where the fuck I am"</td></tr>
|
||||
</tbody></table>
|
||||
<b>High-level uncertainty, </b>aka "Where am I and what an I trying to do?",<b> </b>is when you have a general goal that comes with a set of implicit assumptions. Those assumptions are a minefield you have to traverse as you try to get to your finished product. You end up paralyzed because you're not sure which direction is safe, especially since none of the mines are marked. For example, you might have the task of "In the next month improve our network performance by 50%". It seems like a concrete goal, but there are still a ton of hidden assumptions in there.<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
Solution: Constrain your problem so that what was once vague is now extremely well specified. Constraints tend to dictate solutions. Okay, I have to improve network performance by fifty percent. Can I use more memory to do this? Can I use more CPU? How much more CPU? Can I use a third party library? Is our performance poor sending or receiving or both? Under what conditions? <br/>
|
||||
<br/>
|
||||
Or, using our lost in the woods analog, <i>get a goddamn map.</i><br/>
|
||||
<table><tbody>
|
||||
</tbody></table></div><img src="http://2.bp.blogspot.com/-qOus7B8IArI/UYQI-9JFKRI/AAAAAAAAAOc/me2fCf_E8mc/s400/Greyhawk_1600x1024.jpg"/><div><table><tbody>
|
||||
<tr><td>"So we have a lot of bugs, and apparently orcs and dragons"</td></tr>
|
||||
</tbody></table>
|
||||
Once questions like those are answered the scope of the problem and available solutions becomes a lot clearer, and we can move on to tackling the next problem.<br/>
|
||||
<br/>
|
||||
<b>Strategic uncertainty</b> occurs when you have a well defined goal but you're unsure how to go about it. Using our previous example you've determined that the problem is total network bandwidth, on both sides, and that you can sacrifice a lot of CPU to do it. It seems to happen all the time under all conditions, and unfortunately most of the data going across the network comes from a bunch of different systems so you can't just pare down the data at one location.<br/>
|
||||
<br/>
|
||||
Now you have to come up with a technical solution. You know you need to compress and decompress the data, but how? Maybe you have no experience with compression algorithms, so now you're kind of stuck. Do you just use zlib? Or miniz? Or do you license something? Is there a hardware solution? If there's no clear solution at hand ("drop in zlib") then even very good programmers will spin their wheels, unsure of how to tackle it. They just hope for a brilliant insight.<br/>
|
||||
<br/>
|
||||
Solution: Grind it out by talking to someone. Having an office mate helps tremendously so you can just say "I have to do this thing, I'm not even sure how to tackle it". People like to sound smart and give their opinions, so give someone else the opportunity to solve your problem for you!<br/>
|
||||
<br/>
|
||||
In this case the simplest solution is to use a package like zlib to compress and decompress your data. Strategy defined!<br/>
|
||||
<br/>
|
||||
This leaves the day to day tasks of getting it done, and here's where <b>low-level uncertainty</b> can creep in. Now you know what you're supposed to do at a fairly specific level ("Implement zlib in our codebase so that the network layer is much faster") but at any given moment you're just staring at your editor, unsure what to do next. <br/>
|
||||
<br/>
|
||||
Solution: Make a list. It's that simple. Instead of writing code, start by getting yourself organized and enumerate all your fine grained tasks, no matter how small. "Find compression and decompression entry points". "Get compression code into build system". "Implement dummy implementation to verify integration works." "Write a test harness". etc. Once you have a list, sort in terms of priority. And if you have any tasks that would take longer than a day, go ahead and break them out into smaller tasks.<br/>
|
||||
<br/>
|
||||
You now have a clear road map from A to B, broken down into actionable items at a very fine grained level. There's no more uncertainty. If you're driving, this is the part where your GPS has figured out how to get you from your current location to the nearest Five Guys without having to ask for directions or pull over.<br/>
|
||||
<br/>
|
||||
So what do you do if you're <i>still</i> not getting shit done?! The glib answer is something along the lines of...<br/>
|
||||
<br/>
|
||||
<table><tbody>
|
||||
</tbody></table></div><img src="http://img2u.info/ckgni/i/35cd00e6.jpg"/><div><div><table><tbody>
|
||||
<tr><td>...but this doesn't really help, now does it?</td></tr>
|
||||
</tbody></table>
|
||||
It's likely that you're just not interested in the work. If you're passionate and interested in something, then the work is fun and interesting and you want to do it. Unfortunately a lot of the work we do in software -- in fact, the <i>bulk</i> of work in software development -- is excruciatingly dull. Finding obscure bugs. Dealing with build system issues. Hunting down compiler switches. Typing in the same code we've typed in hundreds of times before. Dealing with poor or missing API documentation.<br/>
|
||||
<br/>
|
||||
For this stuff, you just have to grind. This is where work ethic and discipline can come into play, but I prefer to cheat and just try to stick to things I find interesting. Unfortunately the amount of interesting things to work on in a typical software project is dwarfed by the sheer amount of bullshit tasks.<br/>
|
||||
<br/>
|
||||
<br/></div>
|
||||
<div>
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
Posted <abbr title="2012-09-12T02:26:00.001Z">11th September 2012</abbr> by Unknown
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div></li><li><div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
<a title="8th August 2012" href="http://bookofhook.blogspot.com/2012/08/bug-classification.html">
|
||||
|
||||
|
||||
<div>Aug</div>
|
||||
<div>8</div>
|
||||
<div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<h1>
|
||||
|
||||
|
||||
<a href="http://bookofhook.blogspot.com/2012/08/bug-classification.html" rel="bookmark">
|
||||
|
||||
|
||||
|
||||
Jankiness and the Taxonomy of Bugs</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
<div>Related to my principle of "suffer no jankiness", I've realized that almost all bugs can be distilled into three main categories: <b>I Have No Idea What I'm Doing</b>; <b>Oops</b>; and <b>Oh Fuck You</b>.<br/>
|
||||
<br/>
|
||||
<b>I Have No Idea What's Going On</b> bugs are the worst, because they're due to a lack of comprehension of what's supposed to be occurring in the software (i.e. <a href="http://bookofhook.blogspot.com/2012/08/suffer-no-jankiness.html">jankiness</a>). We attempt to "fix" these bugs by near random transformations until things "work", but of course it's just masked, at best, until it crops up somewhere else.<br/>
|
||||
<br/>
|
||||
Examples of this include:<br/>
|
||||
<br/>
|
||||
<span>v.z = -v.z; // WTF? No idea why this works.</span><br/>
|
||||
<br/>
|
||||
And:<br/>
|
||||
<br/>
|
||||
<span>char buffer[ sizeof( Foo ) + 128 ]; // Add extra just in case...</span><br/>
|
||||
<br/>
|
||||
"In case" of what?<br/>
|
||||
<br/>
|
||||
<span>LaunchThreads();</span><br/>
|
||||
<span>Sleep(1000); // give it some time to startup</span><br/>
|
||||
<br/>
|
||||
"Some time"? Where did the magical 1000ms value come from?<br/>
|
||||
<br/>
|
||||
And:<br/>
|
||||
<br/>
|
||||
<span>if ( !system.Initialized() )</span><br/>
|
||||
<span> system.Initialize(); // Just in case it wasn't initialized already</span><br/>
|
||||
<br/>
|
||||
Any time you see "just in case" in a comment, that's bad.<br/>
|
||||
<br/>
|
||||
<b>I Have No Idea What's Going On</b> bugs are a symptom of flailing about trying to make something work that would probably work if you just took a step back and started over from first principles. But you're in a hurry, you're bored, you're frustrated, and you hope that one more transposition or sign flip will suddenly make everything right.<br/>
|
||||
<br/>
|
||||
These bugs are also really, really bad because <i>they breed</i>. They create more bugs like themselves <i>and </i>they also create a ton of <b>Oh Fuck You</b> bugs for other programmers who have to figure out why the hell you decided that you needed to reset the graphics hardware state every time your function is called "just in case" it wasn't in a known good state.<br/>
|
||||
<br/>
|
||||
<b>Oops</b> bugs are the forgivable ones. They don't show a lack of comprehension of the software, they were just dumb oversights, typos, copy and paste bugs, off-by-1, etc. These are also generally the easiest bugs to hunt down because you're starting with a concrete mental model of how things are supposed to work, and then figuring out why it's not.<br/>
|
||||
<br/>
|
||||
<span>double l = sqrt( v.x*v.x + v.x*v.x + v.x*vx );</span><br/>
|
||||
<br/>
|
||||
Those happen to everyone. Yeah, you can avoid them with better testing and what have you, but by and large they're not symptoms of incomprehension. In my experience each programmer has a fixed rate at which they create these bugs, which is a reflection of their habits.<br/>
|
||||
<br/>
|
||||
<b>Oh Fuck You</b> bugs are the ones where, once you realize what's happened, you say "Oh, <i>fuck you [entity name here]</i>" because someone else has inadvertently sent you on an hours or days long bug hunt that is not of your doing. <br/>
|
||||
<br/>
|
||||
Compilers generate incorrect code, tools corrupt data, SDKs are incorrectly documented, third party libraries do the wrong thing, drivers are broken, someone overclocked their CPU which led to weird memory errors, etc. These bugs tend to occur at a fixed rate irrespective of the individual programmer since they're out of control of the individual programmer.<br/>
|
||||
<br/>
|
||||
In a perfect world you only ever encounter <b>Oh Fuck You</b> bugs, but if you can keep the frequency of <b>Oh Fuck You</b> and <b>Oops</b> bugs higher than those of <b>I Have No Idea What's Going On</b> then you're probably doing pretty well.<br/>
|
||||
<br/></div>
|
||||
<div>
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
Posted <abbr title="2012-08-08T16:30:00.001Z">8th August 2012</abbr> by Unknown
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div></li><li><div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
<a title="8th August 2012" href="http://bookofhook.blogspot.com/2012/08/suffer-no-jankiness.html">
|
||||
|
||||
|
||||
<div>Aug</div>
|
||||
<div>8</div>
|
||||
<div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<h1>
|
||||
|
||||
|
||||
<a href="http://bookofhook.blogspot.com/2012/08/suffer-no-jankiness.html" rel="bookmark">
|
||||
|
||||
|
||||
|
||||
Suffer No Jankiness</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</h1>
|
||||
</div>
|
||||
<div>I'm fairly old (40+) for a programmer, and you'd think most skill jumps would have occurred by now, and I think that in terms of raw ability that's true. Most people learn and apply everything in their first 10 years or whatever of doing things -- their experience will broaden as they get older, which provides a larger set of experiences to pattern match against, but by and large I think their basic 'programming smarts' is set at that point. <br/>
|
||||
<br/>
|
||||
BUT, philosophical outlooks and habits can still grow, and these can materialize as skill jumps. Lately I've been trying to be more intolerant of perceived jankiness in my code.<br/>
|
||||
<br/>
|
||||
A quick definition: code jankiness occurs when the mental model of your software is no longer accurate or complete. Hacks are not janky, for example, as long as you know how and why they work. This, on the other hand, is classic jankiness:<br/>
|
||||
<br/>
|
||||
<span>char buffer[ sizeof( foo ) + 128 ]; // a little extra, just in case...</span><br/>
|
||||
<br/>
|
||||
Every time you see "This should never happen, but just in case", that's janky. In fact, any time you see "Just in case" in a comment -- probably janky. Defensive coding? Total jankiness. Checking against NULL even when there is no reason why that pointer should ever be NULL? Janky.<br/>
|
||||
<br/>
|
||||
"Creeping jankiness" are all the little things that make you go "Huh, that's weird" but you ignore them because, hey, it didn't crash and nothing obviously wrong occurred. The absolute best programmers I've ever known, without exception, are radically intolerant of their software acting in any way that does not match their mental models. If something happens -- no matter how small -- that they do not understand, they stop and go "What just happened?" <br/>
|
||||
<br/>
|
||||
Even if it takes them all day and even if it means that they fall behind in other areas. It seems like a net productivity loss, but it's actually a net productivity win in the long term because you maintain a rock solid understanding of how your software works.<br/>
|
||||
<br/>
|
||||
I can think of at least three great examples of this. The first was when I was at 3Dfx and the Voodoo chip was being developed. Gary Tarolli, 3Dfx's CTO and the principal architect of Voodoo (then known as SST-1), was running some simulations that took a long time (a single frame of rendering would take well over a minute). We'd generate the frames (sometimes over night), then play them back as a movie for demos or whatever.<br/>
|
||||
<br/>
|
||||
<table><tbody>
|
||||
</tbody></table></div></div><img src="http://img2u.info/ckgni/i/a9c5c157.jpg"/><div><table><tbody>
|
||||
<tr><td>Obligatory reminder of that horrible 3Dfx logo</td></tr>
|
||||
</tbody></table>
|
||||
We did this with one of the demos, and during rendering there was a pixel that flickered for exactly one frame. That was it. Hardly noticeable, but there it was. As a software guy I remember kinda shrugging and thinking "Screw it, it looks right most of the time and it didn't crash, SHIP IT!" But Tarolli would not have any of that.<br/>
|
||||
<br/>
|
||||
After spending what seemed like a huge amount of time (I want to say it was at least a day), he found the bug and discovered that it wasn't a bug, but an unintended side effect of a feature that was totally legitimate.<br/>
|
||||
<br/>
|
||||
The second example was when I was at id software. John Carmack was absolutely intolerant of rendering errors -- especially cracks, slivers and T-junctions in his BSP renderer. He was devoutly religious about it, and this is one of the reasons that the Quake games had such an immensely heavy, solid feel to them. <br/>
|
||||
<br/>
|
||||
So of course one day we're testing things, and we see the occasional flash of a crack or a sliver. This was pretty much an all-stop situation for him. He spent a long time (I want to say at least a day -- and one Carmack Day is like Five Regular Dude Days) and finally narrowed it down to an issue specific to an ATI driver or card. Now, most of us would be content to be all "Whew, not my problem, let's move on!" but of course Carmack had to know <i>how</i> the ATI driver was broken.<br/>
|
||||
<br/>
|
||||
It turned out to be their guard band clipper. <a href="http://fgiesen.wordpress.com/2011/07/05/a-trip-through-the-graphics-pipeline-2011-part-5/">Guard band clipping</a> is a graphics driver optimization, but it can introduce cracks when done wrong. If you're going to use guard band clipping, you <i>have</i> to clip against the guard band planes all the time. You can't clip against them for some triangles then clip against the viewport for others -- that can introduce slivers between triangle edges.<br/>
|
||||
<br/>
|
||||
<table><tbody>
|
||||
</tbody></table></div><img src="http://img2u.info/ckgni/i/12fdeffa.jpg"/><div><div><table><tbody>
|
||||
<tr><td>I just needed a Quake 2 screenshot for symmetry...and to remember how goddamn dark that game was</td></tr>
|
||||
</tbody></table>
|
||||
I don't remember if that was a hardware bug or just a driver bug, but the point is that Carmack <i>knew</i> he didn't have those types of rendering errors, and when confronted with them he had to track down what was causing them.<br/>
|
||||
<br/>
|
||||
A more recent example of suffering no jankiness is Casey Muratori's <a href="http://the-witness.net/news/2012/12/finding-and-fixing-a-five-second-stall/comment-page-1/">dissection of a five second Windows stall</a>. It's obvious that once the problem was fixed he could have moved on, but he was compelled to figure out what the hell was going on under the hood so he could have a better understanding of the software's interactions.<br/>
|
||||
<br/>
|
||||
That diligence reaps huge benefits, because very often these small problems are the first cracks in the codebase. By chasing after these now you stop larger problems from erupting later. In addition, by ensuring that only the things that you expect to happen actually happen, you're forced to keep a mental model of exactly what you think is happening in your head. If you become flippant about jankiness it's easy to eject global understanding ("This is how the program works") to focus on localized concerns ("Why is this function crashing?"). Once that mental model of your software is corrupt or vague, you're in major trouble because you no longer know what to expect, and it becomes extremely hard to even detect non-crash bugs because you can't tell the difference between a bug and expected behaviour any more!<br/>
|
||||
<br/>
|
||||
I used to blow off this attitude as overengineering or intellectual wanking -- if it worked, that's what mattered, and I can be getting in a new feature while you're sitting there obsessing over code that works (for the most part...) -- but since then I've decided I've been doing it all wrong, and I need to have a solid understanding of all the code I write, if for no other reason than it makes things easier on me in the long run.<br/>
|
||||
<br/></div>
|
||||
<div>
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
Posted <abbr title="2012-08-08T15:49:00.000Z">8th August 2012</abbr> by Unknown
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div></li></ol><div>
|
||||
<span>Blog Archive</span>
|
||||
</div></div>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<div><img src="https://www.brookings.edu/wp-content/uploads/2021/03/Brad-Marianno.jpg" alt="Brad Marianno" loading="lazy" draggable="false" sizes="80px" srcset="https://i0.wp.com/www.brookings.edu/wp-content/uploads/2021/03/Brad-Marianno.jpg?crop=2px%2C7px%2C650px%2C650px&w=120&ssl=1 120w"/><p>
|
||||
Then came fall 2020. While state leaders took a heavy role in spring 2020 school closures, the dog days of summer unfolded to a more “laissez-faire” attitude among top state executives in regard to the fall 2020 schooling decisions. What followed were agonizing reopening decisions that played out in the nation’s over 13,000 school districts, creating lengthy Zoom school board meetings, such as the <a href="https://www.miamiherald.com/news/local/education/article245906785.html">famous marathon meeting in Miami-Dade</a>. With the venue for school reopening decisions moving from the state to the local level, the teachers’ unions entered the school reopening debate from stage right.
|
||||
</p><h2>
|
||||
Do teachers’ unions shape reopening decisions?
|
||||
</h2><p>
|
||||
The reality is, when reopening decisions were brought into the realm of local control, <a href="https://www.edworkingpapers.com/sites/default/files/ai21-355.pdf">they became far more polarized and political</a>. This should not be terribly surprising. There are <a href="https://www.hepg.org/hep-home/books/the-end-of-exceptionalism-in-american-education_17">far more</a> education interest groups and competing education policy ideas at the state level, making it much more difficult for a single organization to garner a dominant voice among policymakers. When these decisions are brought down into local school board meetings, there remain only a few organizations that are organized enough to exert influence. The teachers’ unions are the largest elephant in the room and the one that, in most school districts around the country, are directly connected to the supply of teachers to the classroom.
|
||||
</p><p>
|
||||
With a team of co-authors, I <a href="http://bradmarianno.faculty.unlv.edu/working-papers/">tracked school reopening decisions</a> throughout the fall 2020 semester in 250 of the largest school districts around the country to understand just how much influence teachers’ unions had on these decisions. We measured teachers’ union power in two ways. The first captures a more fixed measure of power that is built up over a longer period of time: the length of collective bargaining agreement. We think of this <a href="https://www.sciencedirect.com/science/article/abs/pii/S0272775716304691">as a proxy for the detailed working conditions secured by the union.</a> The second is a shorter-term measure, one that captures the reactionary power of teachers’ unions to school reopening decisions. We tracked the Facebook pages of teachers’ unions in our sample for social media posts that made forceful statements on school reopening, including those that discussed lawsuits, membership opinions, and protests.
|
||||
</p><figure><img loading="" alt="Substitute teacher Ryan Giese teaches a fourth grade class Wednesday, October 2, 2019 at Parkside Elementary School in Fond du Lac, Wis. Doug Raflik/USA TODAY NETWORK-WisconsinFon Teacher 100219 Dcr001" src="https://i0.wp.com/www.brookings.edu/wp-content/uploads/2020/10/SUBSTITUTE-TEACHER-RYAN-GIESE-TEACHES-A-FOURTH-GRADE-CLASS.jpg?w=234&crop=0%2C11px%2C100%2C134px&ssl=1" sizes="120px" srcset="https://i0.wp.com/www.brookings.edu/wp-content/uploads/2020/10/SUBSTITUTE-TEACHER-RYAN-GIESE-TEACHES-A-FOURTH-GRADE-CLASS.jpg?w=234&crop=0%2C11px%2C100%2C134px&ssl=1 234w,https://i0.wp.com/www.brookings.edu/wp-content/uploads/2020/10/SUBSTITUTE-TEACHER-RYAN-GIESE-TEACHES-A-FOURTH-GRADE-CLASS.jpg?w=151&crop=0%2C8px%2C100%2C85px&ssl=1 151w,https://i0.wp.com/www.brookings.edu/wp-content/uploads/2020/10/SUBSTITUTE-TEACHER-RYAN-GIESE-TEACHES-A-FOURTH-GRADE-CLASS.jpg?w=135&crop=0%2C7px%2C100%2C76px&ssl=1 135w,https://i0.wp.com/www.brookings.edu/wp-content/uploads/2020/10/SUBSTITUTE-TEACHER-RYAN-GIESE-TEACHES-A-FOURTH-GRADE-CLASS.jpg?w=170&crop=0%2C8px%2C100%2C96px&ssl=1 170w,https://i0.wp.com/www.brookings.edu/wp-content/uploads/2020/10/SUBSTITUTE-TEACHER-RYAN-GIESE-TEACHES-A-FOURTH-GRADE-CLASS.jpg?w=168&crop=0%2C8px%2C100%2C95px&ssl=1 168w"/></figure><div><time>Friday, March 12, 2021</time>
|
||||
</div><p>
|
||||
We find that school districts with lengthier collective bargaining agreements were less likely to start the fall 2020 semester with in-person instruction, were less likely to ever open for in-person instruction during the fall semester, and spent more weeks overall in distance learning. We further find that the shorter-term union power flexes—those captured by the social media posts—were associated with a <em>higher</em> likelihood of reopening for full, in-person instruction during the fall semester. These short-term reactionary responses were not enough to slow the pressure toward reopening; instead, it’s the long-standing, entrenched union power relationships captured by the length of the bargaining agreement that are associated with a slowed reopening timeline.
|
||||
</p><p>
|
||||
But what about COVID-19 rates? We find that pandemic severity, as measured by the county COVID-19 hospitalization rate, had no relationship with the probability of in-person reopening. Instead, the other strongest predictors of an in-person reopening during fall 2020 included political partisanship (the share of Trump voters in the county) and demographics (the percentage of white students in the school district).
|
||||
</p><p>
|
||||
To be clear, we are not the only authors to document that unions, politics, and demographics matter above and beyond pandemic severity, though we are the first to show these relationships beyond the start of the fall 2020 school year. (You can read four other studies with very similar findings <a href="https://www.edworkingpapers.com/sites/default/files/ai21-355.pdf">here</a>, <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3684867">here</a>, <a href="https://www.edworkingpapers.com/sites/default/files/ai20-304.pdf">here</a>, and <a href="https://www.brookings.edu/blog/brown-center-chalkboard/2020/07/29/school-reopening-plans-linked-to-politics-rather-than-public-health/">here</a>.)
|
||||
</p><h2>
|
||||
Teachers’ unions do their job well—but local school governance may be broken
|
||||
</h2><p>
|
||||
So, the empirical research suggests that teachers’ unions slowed fall school reopening decisions during a worldwide pandemic, and media accounts suggest <a href="https://www.edweek.org/teaching-learning/in-chicago-and-other-big-cities-teachers-unions-are-delaying-school-reopenings/2021/02">their efforts</a> continued as schools returned from winter break. Naturally, some will view union actions as an affront to America’s students and others as a public service.
|
||||
</p><p>
|
||||
My hunch is that where one stands right now on the debate around teachers’ unions and school reopening does not depend on nor will it be shaped by the evidence as to their influence on school reopening just presented. Instead, it likely depends on whether and how one has formulated opinions on two other bodies of evidence (some of which has emerged more recently): 1) the evidence on the risks of the pandemic to teachers and students returning to face-to-face instruction, <em>and</em> 2) the evidence on the potential harms caused by distance learning. For those that believe the public health risks of in-person instruction far outweigh the harms caused by distance learning, then teachers’ unions are being unfairly scapegoated for their role in reopening decisions. For those that believe the benefits of in-person instruction far exceed the risks of face-to-face instruction during a pandemic, teachers’ unions are then obstinate, bad-faith actors. I will likely not shift readers opinions here on these fronts, but there is now some solid empirical evidence on the risk of in-person instruction during the pandemic (<a href="https://pediatrics.aappublications.org/content/early/2021/01/06/peds.2020-048090">here</a>, <a href="https://epicedpolicy.org/wp-content/uploads/2020/12/COVID-and-Schools-Dec2020.pdf">here</a>, <a href="https://www.reachcentered.org/publications/the-effects-of-school-reopenings-on-covid-19-hospitalizations">here</a>, and <a href="https://pediatrics.aappublications.org/content/early/2021/03/05/peds.2021-050605">here</a>) and the impact of distance learning (<a href="https://www.gov.uk/government/news/ofsted-children-hardest-hit-by-covid-19-pandemic-are-regressing-in-basic-skills-and-learning">here</a> and <a href="https://www.nwea.org/research/publication/learning-during-covid-19-initial-findings-on-students-reading-and-math-achievement-and-growth/">here</a>).
|
||||
</p><p>
|
||||
We should expect teachers’ unions to slow school reopening decisions just like we should expect them to advocate for increased education funding, lower class sizes, or enhanced teacher sick leave. Protecting and representing teachers is their job and they survive to the extent to which they do this job well. Ever since the Supreme Court’s decision in <em>Janus v. AFSCME</em> (2018) that prohibited the mandatory payment of union fees, unions have had to work harder at this job than ever before (<a href="https://www.educationnext.org/after-janus-new-era-teachers-union-activism-agency-fees/">as I have written elsewhere</a>). Teachers’ unions are like an insurance policy to which teachers will subscribe for days like this—worldwide pandemics where the job becomes substantially more uncertain.
|
||||
</p><p>
|
||||
This should not be mistaken as a ringing endorsement for teachers’ unions to have unfettered influence on reopening conversations. Special interests that represent a few, by definition, exclude many other viewpoints. It is entirely possible, even probable that our localized school governance systems around the country are out of balance—and were this way well before the pandemic. They have become too politically polarized and swayed by entrenched special interests (<a href="https://www.hepg.org/hep-home/books/outside-money-in-school-board-elections">sometimes nationalized special interests</a>). One solution is to increase the marketplace for local education ideas (<a href="https://www.usnews.com/news/education-news/articles/2020-01-13/national-parents-union-to-challenge-political-influence-of-teachers-groups">consider efforts by parents to form their own unions</a>) such that there is greater competition in the local education space. In these situations, special interests become far less special (and far less influential), thereby helping restore a more balanced form of educational governance.
|
||||
</p><p>
|
||||
Ultimately, local education control is not going by the wayside anytime soon (<a href="https://www.hepg.org/hep-home/books/the-end-of-exceptionalism-in-american-education_17">though it is eroding</a>). While some focus their attention toward the teachers’ unions and their role in school reopening decisions, we might also take a hard look at our local school boards and other school governance institutions that decide which voices are heard the loudest when policy decisions are made. The COVID-19 pandemic and school reopening decisions that followed should lead us to scrutinize whether our local educational governance systems are properly calibrated to protect and promote student interests over and above special interests.
|
||||
</p></div>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<div><ul><li><img src="https://i0.wp.com/cavesocial.com/wp-content/uploads/2016/07/shutterstock_529915972.png?fit=1150%2C550&ssl=1" alt="embed instagram posts" srcset="https://i0.wp.com/cavesocial.com/wp-content/uploads/2016/07/shutterstock_529915972.png?resize=200%2C96&ssl=1 200w, https://i0.wp.com/cavesocial.com/wp-content/uploads/2016/07/shutterstock_529915972.png?resize=300%2C143&ssl=1 300w, https://i0.wp.com/cavesocial.com/wp-content/uploads/2016/07/shutterstock_529915972.png?resize=400%2C191&ssl=1 400w, https://i0.wp.com/cavesocial.com/wp-content/uploads/2016/07/shutterstock_529915972.png?resize=600%2C287&ssl=1 600w, https://i0.wp.com/cavesocial.com/wp-content/uploads/2016/07/shutterstock_529915972.png?resize=768%2C367&ssl=1 768w, https://i0.wp.com/cavesocial.com/wp-content/uploads/2016/07/shutterstock_529915972.png?resize=800%2C383&ssl=1 800w, https://i0.wp.com/cavesocial.com/wp-content/uploads/2016/07/shutterstock_529915972.png?resize=1024%2C490&ssl=1 1024w, https://i0.wp.com/cavesocial.com/wp-content/uploads/2016/07/shutterstock_529915972.png?fit=1150%2C550&ssl=1 1150w" sizes="(max-width: 800px) 100vw, 1150px" loading="eager" draggable="false"/></li></ul><p>
|
||||
Embedding photos is a great way to legally show other people’s photos on your website, promote their content, and entice them to share your blog post on their social accounts.
|
||||
</p><p>
|
||||
Let’s say you want to write about restaurants in your area. These articles are broad and cast a wide net in terms of readership. You’ll be capturing traffic from people in your area, and providing them with valuable content that presents you as a local expert. Awesome!
|
||||
</p><p>
|
||||
Write your article, link to the restaurants, and then – as the title of this article implies – embed photos from Instagram onto your blog.
|
||||
</p><p>
|
||||
Here’s what you do.
|
||||
</p><h2>
|
||||
1. Find the restaurant on Instagram
|
||||
</h2><p>
|
||||
For example, if you mentioned Zeeks Pizza in your article, head to Instagram and search ‘Zeeks Pizza,’ or just type, ‘Zeeks Pizza Instagram,’ into Google, then follow the link.
|
||||
</p><img src="https://i1.wp.com/cavesocial.com/wp-content/uploads/2016/07/zeeks-pizza-instagram-Google-Search.clipular.png?resize=808%2C259&ssl=1" alt="embed instagram photos" loading="eager"/><h2>
|
||||
2. Find the best photo for your article
|
||||
</h2><p>
|
||||
Take a look through their photos, and find one that you think will speak to your audience. We recommend going for a photo of the restaurant’s signature dish, in this case, pizza. When you find a winner, click on it. Of the three below, we’d recommend the one of the far left.
|
||||
</p><img src="https://i1.wp.com/cavesocial.com/wp-content/uploads/2016/07/Zeeks-Pizza-@zeekspizza-Instagram-photos-and-videos.clipular-1.jpg?resize=975%2C324&ssl=1" alt="embed an instagram photo to your blog" loading="eager"/><h2>
|
||||
3. Click the 3 dots in the bottom, right-hand corner
|
||||
</h2><img src="https://i2.wp.com/cavesocial.com/wp-content/uploads/2016/07/click-three-buttons-instagram.png?resize=600%2C438&ssl=1" alt="embed on instagram" loading="eager"/><h2>
|
||||
4. Click the embed button
|
||||
</h2><img src="https://i2.wp.com/cavesocial.com/wp-content/uploads/2016/07/embed-instagram-photo.png?resize=600%2C437&ssl=1" alt="embed on instagram" loading="eager"/><h2>
|
||||
5. Copy the embed code
|
||||
</h2><img src="https://i2.wp.com/cavesocial.com/wp-content/uploads/2016/07/click-embed-button.png?resize=501%2C221&ssl=1" alt="how to imbed photos on instagram" loading="eager"/><h2>
|
||||
6. Paste the embed code in the text of your website
|
||||
</h2><p>
|
||||
If you use WordPress, paste the embed code in the text portion of your editor. From there, you’re good to go. Your new Instagram photo will be embedded in your blog post! It should look something like this:
|
||||
</p><p>
|
||||
<br/>
|
||||
Once you’ve finished your blog, be sure to share it with all of the restaurants you mentioned. You’ll usually see a few of the restaurants share the blog post on their social media accounts, bringing more visitors to your website (along with more potential customers).
|
||||
</p><h2>
|
||||
7. Center the content
|
||||
</h2><p>
|
||||
When you put an Instagram embed code on your website, it will default to left alignment. If you want to center your content you have a few options.
|
||||
</p><ol><li>You can add a <center></center> tag to the beginning and end of the embed (this will have to be done on every post)</li><li>In WordPress you can add this as additional CSS to ensure every post is centered: <strong>.instagram-media{margin:15px auto !important;}</strong></li></ol><p>
|
||||
Watch our <a href="https://cavesocial.com/write-blog-hits-1000-views-episode-15/">video on how to get your +1000 visitors to your next blog post</a> for more tactics, and be sure to email me at jordan[at]cavesocial.com if you have any questions.
|
||||
</p><p>
|
||||
<em>Be sure to tweet any questions you have to the Cave Team on Twitter – just use the hashtag <a href="https://www.twitter.com/cavesocial">#AskCave</a></em>
|
||||
</p></div>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<div><figure><picture><img loading="lazy" src="https://onecms-res.cloudinary.com/image/upload/s--OJC_brzs--/c_fill%2Cg_auto%2Ch_468%2Cw_830/fl_relative%2Cg_south_east%2Cl_mediacorp:cna:watermark:2021-08:cna%2Cw_0.1/f_auto%2Cq_auto/v1/mediacorp/cna/image/2021-09/dsc_3547.jpg?itok=aYAAo50I" alt="New points system for Employment Pass applicants from September 2023" title="New points system for Employment Pass applicants from September 2023"/></picture><figcaption>People wearing protective face masks waiting to cross the road at Church Street on Sep 6, 2021. (Photo: CNA/Gaya Chandramohan)</figcaption></figure><p>
|
||||
SINGAPORE: On top of meeting a minimum qualifying salary, foreigners wanting to work in Singapore under an Employment Pass (EPs) will also have to meet a minimum score under a points system from September next year.
|
||||
</p><p>
|
||||
The new framework comes as the Ministry of Manpower (MOM) is set to adjust the minimum qualifying salary for EPs up from S$4,500 to S$5,000 from this September. For the financial sector, the salary benchmark is set higher at S$5,500.
|
||||
</p><h2>
|
||||
Advertisement
|
||||
</h2><p>
|
||||
While S$5,000 or S$5,500 is the minimum salary for entry-level jobs, it goes up to S$10,500 for middle-aged candidates, or S$11,500 if they are in the financial sector.
|
||||
</p><p>
|
||||
The higher wage benchmark will apply to new EP applications from Sep 1 this year, and to renewal applications a year later.
|
||||
</p><p>
|
||||
EP applicants generally fill professionals, managers, engineers and technicians (PMET) jobs.
|
||||
</p><p>
|
||||
The intent is for EP qualifying salary to match the top one-third, or 65th percentile, of local PMET wages by age, said Manpower Minister Tan See Leng, as he announced the upcoming changes in Parliament during MOM’s Committee of Supply debate on Friday (Mar 4).
|
||||
</p><p>
|
||||
“We know that the labour market for skilled workers is sufficiently tight. So this latest move is not intended as a tightening measure,” he said.
|
||||
</p><h2>
|
||||
Advertisement
|
||||
</h2><p>
|
||||
“Instead, it is part of MOM’s regular updates to ensure that our qualifying salary keeps pace with local wage growth, so that foreigners are not coming in just because they are cheaper than local PMETs.”
|
||||
</p><p>
|
||||
He added that most EP holders will not be affected by these changes as they earn “well above the qualifying salary”.
|
||||
</p><p>
|
||||
From next year, applicants will also have to score sufficient points under a new COMPASS (Complementarity Assessment Framework) points system to work here.
|
||||
</p><p>
|
||||
It will come into effect for new EP applications from September 2023, and for renewal applications from September 2024.
|
||||
</p><h2>
|
||||
HOW COMPASS WORKS
|
||||
</h2><p>
|
||||
Points under COMPASS will be awarded based on four attributes and two bonus criteria.
|
||||
</p><h2>
|
||||
Advertisement
|
||||
</h2><p>
|
||||
The EP application can score up to 20 points for each attribute – the applicant’s salary, his or her qualifications, the nationality diversity of the hiring firm and the firm’s support for local employment.
|
||||
</p><p>
|
||||
An individual and/or firm that meet the expectations for each attribute or criteria will score 10 points and if it exceeds expectations, it scores 20 points.
|
||||
</p><figure><img src="https://onecms-res.cloudinary.com/image/upload/s--yEmziZKn--/f_auto%2Cq_auto/v1/mediacorp/cna/image/2022/03/04/20220303-compass.png?itok=CZgUtvc_" alt=""/></figure><p>
|
||||
The applicant or firm can also score on two bonus criteria. One is a “Skills Bonus” for candidates in jobs where skills shortages exist, which can add up to 20 points.
|
||||
</p><p>
|
||||
The second is a “Strategic Economic Priorities Bonus” of up to 10 points – for firms that engage in innovation and internationalisation activities in partnership with the Government.
|
||||
</p><p>
|
||||
For example, applicants with a salary that meets the 65th to 90th percentile of local PMET salaries in the sector will score 10 points under the “Salary” attribute. They will get another 10 points under the “Qualifications” attribute if they hold a degree or its equivalent.
|
||||
</p><h2>
|
||||
Advertisement
|
||||
</h2><p>
|
||||
If the candidate exceeds expectations - like if his or her salary is above the 90th percentile, or if their educational qualifications are from a “top-tier institution”, they score 20 points.
|
||||
</p><p>
|
||||
There are also two attributes for the firm to meet.
|
||||
</p><p>
|
||||
The firm can score 10 points if the proportion of its employees with the same nationality as the applicant is between 5 and 25 per cent. It scores 0 if the proportion is more than 25 per cent, and 20 points if it is less than 5 per cent.
|
||||
</p><p>
|
||||
It scores another 10 points if the firm’s share of local PMETs is at the 20th to 50th percentile compared to its subsector.
|
||||
</p><figure><img src="https://onecms-res.cloudinary.com/image/upload/s--kwKhV4qM--/f_auto%2Cq_auto/v1/mediacorp/cna/image/2022/03/04/20220303-scoring.png?itok=chxEUekv" alt=""/></figure><p>
|
||||
Small firms with fewer than 25 PMET employees score 10 points on the diversity and support for local employment attributes by default.
|
||||
</p><p>
|
||||
This is as MOM recognises that for smaller firms, their workforce ratios can “change drastically” with a few personnel changes, said Dr Tan.
|
||||
</p><p>
|
||||
If the application meets expectations on all four attributes, it should pass with 40 points. If not, it could be approved using the bonus criteria.
|
||||
</p><p>
|
||||
Dr Tan said that COMPASS is not designed to make it harder for companies to obtain an EP and a majority of the applications today would pass.
|
||||
</p><p>
|
||||
“This is because our businesses in general are progressive, and our policies thus far have brought in complementary EP holders,” he said.
|
||||
</p><p>
|
||||
“Some firms, however, would need to make adjustments under COMPASS. And they would know clearly which areas to improve on and how to respond accordingly, given the transparency, clarity, and predictability of COMPASS.”
|
||||
</p><p>
|
||||
Dr Tan also said that the ministry has received feedback from employers who are concerned about bringing in young, high-potential employees under global rotation programmes.
|
||||
</p><p>
|
||||
To facilitate this, the ministry will pilot the Global Rotation Scheme, and the firm must also post Singaporeans abroad on the same leadership development programme.
|
||||
</p><div>
|
||||
Source: CNA/hm(cy)
|
||||
</div></div>
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<div><p>
|
||||
SINGAPORE: Seven years.
|
||||
</p><p>
|
||||
Of pressure, of pain, of doubt. Of trying harder, of getting faster, of growing wiser.
|
||||
</p><h2>
|
||||
Advertisement
|
||||
</h2><p>
|
||||
There are now brown streaks in her hair, seven tattoos inked on her skin, and three goals on her mind.
|
||||
</p><p>
|
||||
Give everything. Enjoy yourself. Send a message.
|
||||
</p><p>
|
||||
And as she prepared herself for the women's 200m final at the 31st SEA Games in Hanoi, sprinter Shanti Pereira would repeat a mantra. Over and over and over again.
|
||||
</p><p>
|
||||
"No matter what happens, you'll be good."
|
||||
</p><figure><img src="https://onecms-res.cloudinary.com/image/upload/s--vFj831Mo--/c_fill%2Cg_auto%2Ch_468%2Cw_830/fl_relative%2Cg_south_east%2Cl_one-cms:core:watermark:ap_data-1%2Cw_0.1/f_auto%2Cq_auto/v1/one-cms/core/vietnam_sea_games_56918.jpg?itok=UFu9Mduo"/><figcaption>From left to right, Pereira Veronica Shanti of Singapore, Kyla Ashley Richardson Maico, Kyla Anise Richardson Maico of Philippines and Hoang Thi Ngoc of Vietnam compete during the women's 200M run in the Athletics competition during the 31st Southeast Asian Games (SEA Games 31) in Hanoi, Vietnam Saturday, May 14, 2022. (AP Photo/Minh Hoang)</figcaption></figure><h2>
|
||||
<strong>SELF-DOUBT</strong>
|
||||
</h2><p>
|
||||
To understand what the race felt like to Pereira, one has to go back to the 2015 Games in Singapore.
|
||||
</p><h2>
|
||||
Advertisement
|
||||
</h2><p>
|
||||
At the meet, she would win gold in the 200m, clock a personal best and set a new national record. The win also ended a 42-year gold medal drought for Singapore in a Games sprint event.
|
||||
</p><p>
|
||||
A new sprint queen had arrived.
|
||||
</p><p>
|
||||
“I was just excited to be there, competing in front of a home crowd,” recalled Pereira, who also clinched a bronze in the 100m.
|
||||
</p><p>
|
||||
But with the weight of winning came the enormity of expectations.
|
||||
</p><p>
|
||||
“It created a lot of pressure and I feel like I didn't have the experience to cope with it yet,” she explained.
|
||||
</p><h2>
|
||||
Advertisement
|
||||
</h2><p>
|
||||
The next edition of the Games would end in tears for Pereira, as she finished with a pair of bronzes in the 100m and 200m.
|
||||
</p><p>
|
||||
Then came two injuries in 2018, before she would finish with two bronzes again at the 2019 Games.
|
||||
</p><p>
|
||||
"Physically for the longest time, I was there. I (felt I) could do a PB (personal best), NR (national record) and all that but mentally I wasn't there," she said.
|
||||
</p><p>
|
||||
"There was a lot of self-doubt that kind of grew as the years went by. Just because it was like - again another season I couldn't get a PB, and it just continued and continued and continued."
|
||||
</p><figure><img src="https://onecms-res.cloudinary.com/image/upload/s--_YxbqbOX--/c_fill%2Cg_auto%2Ch_468%2Cw_830/f_auto%2Cq_auto/v1/cna-migration/singapore-s-shanti-pereira.jpg?itok=SZt91-79"/><figcaption>Singapore's Shanti Pereira competes in the women's 200m heats at the Tokyo Olympics on Aug 2, 2021. (Photo: Facebook/Singapore National Olympic Council/Kong Chew Yew)</figcaption></figure><p>
|
||||
And some people began to write her off, said Pereira.
|
||||
</p><h2>
|
||||
Advertisement
|
||||
</h2><p>
|
||||
"Slowly, people just kind of lost faith in me. People have their opinions about my journey, and whatnot. I think, for a while there, I really did let it get to me, which is not ideal. I want to be in a state mentally strong and things like that don't affect me, but for a while it really did."
|
||||
</p><p>
|
||||
The negativity ate her up, added Pereira. It got to the point that there were times when she was no longer excited to compete or even train.
|
||||
</p><p>
|
||||
Instead, what she felt was fear.
|
||||
</p><p>
|
||||
"I couldn't find any motivation," she recalled. "Coming to training, I was just always afraid of doing bad times at training and just not doing well at training."
|
||||
</p><h2>
|
||||
<strong>FEEDING OFF A DIFFERENT ENERGY</strong>
|
||||
</h2><p>
|
||||
Sandwiched among the lows were highs too.
|
||||
</p><p>
|
||||
Pereira clocked a new 100m PB of 11.58 seconds at the 2019 Asian Athletic Championships, and also made her Olympic debut in Tokyo where she ran a season's best time in the heats.
|
||||
</p><p>
|
||||
But even with the Olympics, 2021 was a "rough" year, revealed Pereira.
|
||||
</p><p>
|
||||
Off the track, she decided to pursue a career unrelated to her Singapore Management University accountancy degree. On the track, the results were not "showing up".
|
||||
</p><p>
|
||||
"I felt like I suffered a bit of an identity crisis. I started to believe what people said about me, that maybe I’ve reached my end. And if so, who was I?" she said.
|
||||
</p><p>
|
||||
But speaking to the ones close to her helped.
|
||||
</p><p>
|
||||
"They've helped me a lot, to realise that I am fine, I am okay. It's really just a change of a mindset that I had to try to do," she said.
|
||||
</p><p>
|
||||
In the lead up to this year's Games, Pereira did just that.
|
||||
</p><p>
|
||||
"I reached a point where I just told myself that it really doesn't matter what people think of you," she said. "I have people in my corner, and they really believe in me. They have absolute trust in me that I am not at the end of my career. And I think I just fed off that energy more."
|
||||
</p><p>
|
||||
She looked inward.
|
||||
</p><p>
|
||||
"I can only focus on the things that I can control - which is how I approach the Games, how I approach training, all these things," she said.
|
||||
</p><p>
|
||||
"Whatever people are gonna say, they're gonna say. It doesn't matter. What matters is how I do things, making sure that I do whatever I can and give absolutely everything to my races."
|
||||
</p><img src="https://onecms-res.cloudinary.com/image/upload/s--7uAHXeAV--/c_crop%2Ch_3285%2Cw_5840%2Cx_0%2Cy_57/c_fill%2Cg_auto%2Ch_98%2Cw_175/f_auto%2Cq_auto/v1/mediacorp/cna/image/2022/05/14/20220514_athletics_reu_002.jpg?itok=YsKmIsDc"/><h6>
|
||||
<a href="/sport/shanti-pereira-wins-200m-gold-sea-games-new-national-record-2685176"><span>Shanti Pereira wins 200m gold at SEA Games in new national record</span></a>
|
||||
</h6><p>
|
||||
And going into this year's Games, there was a sense of calm, recalled Pereira.
|
||||
</p><p>
|
||||
"That was ultimately what I wanted. I wanted to just chill. This is my thing, I'm just going to go there and do the best I can."
|
||||
</p><p>
|
||||
And this would mean channelling a similar outlook that she had seven years ago.
|
||||
</p><p>
|
||||
"That's the kind of mindset I wanted to be in. I wanted to just relax and have fun and just enjoy the fact that I'm here racing," she said.
|
||||
</p><h2>
|
||||
<strong>'I'M STILL HERE, I'VE STILL GOT IT"</strong>
|
||||
</h2><p>
|
||||
Her legs are a whir, her heart thumping like a drum, her eyes fixed on the finish line.
|
||||
</p><p>
|
||||
At the My Dinh Stadium in Hanoi, along the final stretch of the 200m race, Pereira is ahead.
|
||||
</p><p>
|
||||
To her left, a Filipino opponent gains on her, but the Singaporean is not letting this one slip away. Not today.
|
||||
</p><figure><img src="https://onecms-res.cloudinary.com/image/upload/s--GaMVSd4C--/c_fill%2Cg_auto%2Ch_468%2Cw_830/fl_relative%2Cg_south_east%2Cl_one-cms:core:watermark:ap_data-1%2Cw_0.1/f_auto%2Cq_auto/v1/one-cms/core/vietnam_sea_games_20701.jpg?itok=XMgAlyrp"/><figcaption>Gold medalist Pereira Veronica Shanti of Singapore celebrates with the national flag after the women's 200M run in the Athletics competition during 31st Southeast Asian Games (SEA Games 31) in Hanoi, Vietnam Saturday, May 14, 2022. (AP Photo/Minh Hoang)</figcaption></figure><p>
|
||||
Personal best. National record. Gold medal.
|
||||
</p><p>
|
||||
The 18-year-old with the blue glitter hairband is gone. There’s no exuberant waving to the crowd, no gleeful smile of joy.
|
||||
</p><p>
|
||||
Instead, the 25-year-old is floored. Exhaustion, disbelief and the sheer magnitude of what she has done overwhelm her.
|
||||
</p><p>
|
||||
"I told myself before the finals that no matter what the result is, I'm going to give 100 per cent. Everything I have, physically, mentally, emotionally. Everything," Pereira explained.
|
||||
</p><p>
|
||||
Her message - not just to others, but also to herself - rang loud and clear.
|
||||
</p><p>
|
||||
"I’m still here, I’ve still got it," she said. Days later, Pereira would add another medal to her tally, a silver in the 100m.
|
||||
</p><p>
|
||||
Even till today, what she achieved at the Games still hasn't fully sunk in. And isn't just about the victory or the timing, but the "breakthrough", she explained.
|
||||
</p><p>
|
||||
"I put in a lot of hard work and sacrifice to be able to make that breakthrough," she explained. "This wasn't the first time where I wanted to do that. I wanted to do this for a really long time. The fact that I actually did it is quite amazing to me."
|
||||
</p><p>
|
||||
Pereira's coach Luis Cunha said he was confident in Pereira's ability to perform the way she did.
|
||||
</p><p>
|
||||
"(With the performance at the SEA Games) she showed herself that she was able to clock the times. But she also knew what I told her (previously) - that she can do it," he said.
|
||||
</p><p>
|
||||
"Now she can confirm my thoughts with the performance. I hope that this allows her to go further and continue to improve this season."
|
||||
</p><figure><img src="https://onecms-res.cloudinary.com/image/upload/s--7sXJd_AH--/c_crop%2Ch_2025%2Cw_3600%2Cx_0%2Cy_5/c_fill%2Cg_auto%2Ch_468%2Cw_830/f_auto%2Cq_auto/v1/mediacorp/cna/image/2022/06/01/shanti_sea_games.jpeg?itok=5ATUZETU" alt=""/><figcaption>Shanti Pereira poses with her SEA Games gold medal. (Photo: SportSG/Jeremy Lee)</figcaption></figure><h2>
|
||||
<strong>A NEW CHAPTER</strong>
|
||||
</h2><p>
|
||||
Being able to continue with the same mindset remains a work in progress, noted Pereira.
|
||||
</p><p>
|
||||
"It's still a process. I learned from what I did at the SEA Games, and I want to continue doing that. But it's still hard."
|
||||
</p><p>
|
||||
And she will aim to build on this, with the Commonwealth Games and the World Athletics Championships still to come later this year.
|
||||
</p><p>
|
||||
"It took so much out of me to be able to have that mindset, so I just want to learn from it and continue with this mindset," Pereira added.
|
||||
</p><p>
|
||||
Pereira has yet to to have a discussion with Cunha on her specific goals for these meets. But she already knows, at the very least, she wants to bring down her times.
|
||||
</p><p>
|
||||
"I want to go faster (than I did at the SEA Games), for sure. And I feel like I can. That's the goal. Just do whatever I can in the next few months to try and hopefully go faster for both the 100m and 200m."
|
||||
</p><img src="https://onecms-res.cloudinary.com/image/upload/s--T7QfbFxI--/c_crop%2Ch_843%2Cw_1500%2Cx_0%2Cy_38/c_fill%2Cg_auto%2Ch_98%2Cw_175/f_auto%2Cq_auto/v1/mediacorp/cna/image/2022/05/18/dl_0ccn4.jpeg?itok=Z6MfPL7q"/><h6>
|
||||
<a href="/sport/shanti-pereira-and-marc-brian-louis-win-silver-and-bronze-sea-games-respective-100m-events-2690506"><span>Shanti Pereira and Marc Brian Louis win silver and bronze at SEA Games in respective 100m events</span></a>
|
||||
</h6><p>
|
||||
After Pereira's first Games gold, her older sisters Valerie and Shobi were inspired to write a children’s book to tell the story of her journey and triumph.
|
||||
</p></div>
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<div><p>
|
||||
<a href="https://link.chtbl.com/4cfCdW13">This week’s ChinaTalk episode</a> with economic historian <a href="https://twitter.com/adam_tooze">Adam Tooze</a> and guest host <a href="https://twitter.com/m_c_klein?lang=en">Matt Klein</a> was fantastic. See below for a condensed transcript.
|
||||
</p><h1>
|
||||
Do Fiscal Constraints Even Matter?
|
||||
</h1><p>
|
||||
<strong>Matt</strong>: A lot of what we think of as financial constraints can be overcome by the determination to mobilize resources in the wartime economy. There are still real economic constraints in terms of labor shortages or access to raw materials. But conventional financial constraints, like how much debt there is, turn out to be somewhat less important. Yet despite the knowledge that we should have had from previous experience, most governments -- with the notable exception of China and Russia -- basically acted as if they were constrained by these sorts of financial circumstances.
|
||||
</p><p>
|
||||
<strong>Adam</strong>: The history of the long 20th century suggests that <strong>there is this remarkable freedom in the modern economy with the resources of the modern States to mobilize financial and monetary means to do the things that we want to do.</strong> In European history and in global history, the early 20th century provided several lessons along these lines.
|
||||
</p><p>
|
||||
The first experience of this type was World War One, which was a sort of learning by doing. As I was showing in <em><a href="https://en.wikipedia.org/wiki/The_Wages_of_Destruction">Wages of Destruction</a></em> when I was looking at Nazi Germany in the 1930s, there was quite a deliberate effort to use financial and monetary means to mobilize the real economy to the absolute limit of its potential.
|
||||
</p><p>
|
||||
To be a little trite you could say that the lesson learned from the genealogy of European neo-liberalism was that the potential is there. But in that potential lies the possibility of political disaster. Keynesians may look at this scenario and say, that's great, the problem with unemployment is solved. But the neoliberal will stand back and say, hang on, you're being naive. What you're not understanding is that this basically unfetters politics. Sure if you're dealing with a bunch of herbivorous Social Democrats, they'll take you in one direction and you'll end up with a welfare state and full employment, but if their same knowledge is in the hands of a group of nationalist militarists, what you've really provided them with is the blueprint for highly efficient mobilization of a military economy in times of peace. <strong>So deep in the heart of neoliberal thought and conservative thinking about the modern state and its potential lies a fear of that possibility.</strong>
|
||||
</p><p>
|
||||
If you were looking for just a simple segue from the history of Nazi Germany and the history of the experience of the total war economies to the present, it would be by way of that backflip and the recognition of the danger that kind of politics potentially poses.
|
||||
</p><h3>
|
||||
<strong>I do think at the heart of modern conservatism is a fear of the realization that really there isn't that much that constrains us</strong> <em><strong>other</strong></em> <strong>than the real constraints.</strong>
|
||||
</h3><p>
|
||||
Because when you get to actually debating the real constraints, it gets political very fast. It’s immediately a series of very tough tradeoffs and you have to formulate what the purposes of your government are. That in itself is an explosive proposition. So, in a sense to say, “well there's financial limits, and once public debt exceeds 90% of you fall down a slippery slope,” that short circuits a bunch of really profound political questions that one would probably rather avoid.
|
||||
</p><h1>
|
||||
How Contingent Was WWII?
|
||||
</h1><p>
|
||||
<strong>Matt</strong>: The popular narrative is that the post-World War One regime was incredibly unstable and bound to break down. Your argument is that's completely wrong, it was remarkably durable and took a very unusual combination of circumstances to even put it under threat. Then even then the challenger powers basically, with exception of the USSR, didn't really end up any better off than they were at breaking this Anglo-American dominance. Thinking about the theme of this podcast and where we are now, what would you take away from this in thinking about the durability of the post-1991 world order? How should we think about that in the context of sort of U.S.-China competition? If you take this 1920s analogy seriously, that suggests you basically need some kind of extreme series of crises in the U.S. to change things seriously.
|
||||
</p><p>
|
||||
<strong>Adam</strong>: It would be unwise to draw that conclusion. It’s a wonderful summary of <em><a href="https://www.amazon.com/Deluge-America-Remaking-Global-1916-1931/dp/0143127977">The Deluge</a></em>, which was essentially an argument with German theorists like <a href="https://plato.stanford.edu/entries/schmitt/">Carl Schmitt</a>, who insisted that the problem with liberal internationalism and the League of Nations was not that they were weak, the problem was that they were hypocritical. In other words, [liberal internationalism] was shamefaced about the propensity and capacity for violence that actually underpinned it, which revealed itself when Imperial Japan, Fascist Italy, and Nazi Germany made the challenge. Why did Hitler have to try as hard as he did? This is not a conventional, familiar sort of arms race; this is a massively unstable tumbling forward into a completely unstable arms race economy.
|
||||
</p><p>
|
||||
Why were they forced to make that kind of effort? An interest in the 1920s revived in the 1990s amongst international historians, because we were in a sense thinking about the analogies between the new unipolar order taking shape after the end of the Cold War, which was inflected in various ways with ideologies of liberal internationalism that did in the American case, trace back to The Wilsonian moment. That's what drew folks like me into thinking about this. It drew folks like <a href="https://en.wikipedia.org/wiki/Niall_Ferguson">Niall Ferguson</a> into thinking about it too, from the other side, in other words, as a celebrant of a muscular, organizing Imperial hegemonic power.
|
||||
</p><p>
|
||||
But I would argue that over the course of the last couple of decades, I've become ever more fascinated by what I take to be the world-historic transition that we're witnessing. Because China’s not the Soviet Union, China's not fascist Italy, China's not Nazi Germany. The growth of China is a phenomenon that dwarfs all of those previous developments and has to be understood on the timeframe that was laid out for us by the economic data of somebody like <a href="https://en.wikipedia.org/wiki/Angus_Maddison">Angus Maddison</a>, who shows us global GDP all the way back to the birth of Christ. All the way through the beginning of the 19th century, the Asian economies actually dominate once you've adjusted GDP by purchasing power parity and so on.
|
||||
</p><img src="https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F782aa28c-260e-44a8-b47a-7fc9237a2a18_1034x549.jpeg" alt="Over 2,000 Years of Economic History in One Chart"/><p>
|
||||
I quite like what Larry Summers said, which is that the question is whether there's any political force in the United States that's capable of really reckoning with the scale of this world-historic shift, which will in the end lead to the supersession of America's dominant position. <strong>We will end up in some sort of multi-polar order and the question is whether any political forces in the United States can really accept that kind of transition.</strong> It's tempting to say, is there anyone in the United States that could play the role of the British elite after World War One? But America's position of dominance was vastly greater than that ever by enjoyed the British so the psychological challenge of accepting this transition is far greater. And, of course, in key respects America remains an absolutely dominant player, most notably with regard its hard power, its weapons, but also in certain respects with regard to its financial centrality.
|
||||
</p><p>
|
||||
I'd be very cautious about drawing a straight line. There is a connection in the sense that one thing grows out of the other. In <em>The Deluge</em> I spent lots of time trying to position the history of the emergence of the Chinese Republic in the epoch that we normally think of as being about Europe and the United States.
|
||||
</p><h1>
|
||||
What Can Nazi Germany Teach Us About the CCP?
|
||||
</h1><p>
|
||||
<strong>Jordan</strong>: Coming to another one of your books, <em>Wages of Destruction</em>, which looks at the Nazi Germany economy: Is there anything we can learn today from the Nazi economy for thinking about China today?
|
||||
</p><p>
|
||||
<strong>Adam</strong>: I wouldn't go straight at it. I would go by way of political theory. The person that I would go to is <a href="https://en.wikipedia.org/wiki/Franz_Neumann_(political_scientist)">Neumann</a>, the German political theorist who wrote an extraordinary book called <em><a href="https://www.amazon.com/Behemoth-Structure-Practice-Socialism-1933-1944/dp/1566638194">Behemoth</a></em>. He described what happens to capitalist States when they get caught up in a logic, which he took to be characteristic of the 20th century, which is oligopolistic, monopolistic rule. The international system of free trade results from monopoly. And the result of that is protectionism and tariff war, and, ultimately in Hobson's view, a race towards imperialist competition and war. Continue that line of thought and ask yourself, “Okay, so what does monopoly do and what do oligopolistic structures do to the domestic political structure?”
|
||||
</p><p>
|
||||
It systematically subverts principles of representation. It begs the question of who has power and what is the role of the state in relation to those interests. Because properly understood liberalism clearly isn't premised on the absence of the state, its premised on a well-ordered set of relationships between individuals, the law and various types of representation. That structure is not necessarily robust if economic power becomes monolithic. There are ways of taming that by way of corporatism, in which you have an organized representation of economic interests. But you can also imagine systems in which it can become a sort of destructive set of flywheels of extremely explosive dynamics of gigantic interest groups contending with each other more or less in an unmediated direct form interest on interest.
|
||||
</p><p>
|
||||
I think a pessimist would say, you've just described modern Western societies. But I think you could also ask how useful that analytics might be for thinking about the situation in a highly complex political economy like China, which of course didn't start out liberal. And as we increasingly are coming to terms with is unlikely to develop in a liberal direction. But it does seem to be characterized by a clash within the one-party state of different interests, of agglomerations of technology and capital, of different party factions.
|
||||
</p><p>
|
||||
I would love for us to have a Neumann, <em>Behemoth</em> style treatment of China. I'm not saying that the results would be the same, but that analytic would be profoundly helpful to try to understand what's going on there.
|
||||
</p><p>
|
||||
<strong>Matt</strong>: One thing I think is interesting is that the way that, the Nazi economy worked, it could have been described as capitalist, insofar as how Stalin defined the capitalist bloc. You did have private companies and you did have concentrations of wealth that were outside of the formal structures of the state. Yet there also was a very real sense in which these private enterprises and private capital weren't really independent of the state in the way they might've been in another country.
|
||||
</p><p>
|
||||
The extent to which the Nazis were able to marshal the resources the way they did was by allying German big businesses to their project through a mixture of rewards and penalties. That mixture of party-political objectives with the interests of these sort of quasi-private enterprises and elites, you can draw links to the structure of the modern Chinese economy as well. I wouldn't want to take the analogy too far, but I think there is a parallel. You can argue it's for similar political reasons in so far as you have a government with different factions and competing interests within it that we can't really observe. But at the same time, the way the Party is able to drive economic activity – I think there's definitely some interesting analogies there,
|
||||
</p><p>
|
||||
<strong>Adam</strong>: In my reference to Neumann I want to emphasize that part of the commonality <em>is</em> the confusion and complexity. Our categories don't fit very well. It's quite difficult to describe the Chinese regime in categories that are at all familiar without reducing it hopelessly and just failing to recognize its complexity. It's that position of puzzlement that strikes me as analogous. I'm not saying that China's Nazi Germany – the problem is similar, though, in that in analyzing it, none of our categories seem to fit it very well.
|
||||
</p><p>
|
||||
<strong>This seems reminiscent of the difficulty that liberal analysts in the thirties and forties had in looking at Nazi Germany</strong>. They were not radically othering Nazi Germany. They weren't engaged in the crude Stalinist stereotypes of saying, “it's capitalist and your capitalist, so basically you are the same,” which is hopelessly reductive. There is a version, as it were, of the left-wing analysis of China today which is willing to say that, that it's just state capitalism and that's all there is to it. I don't find that position at all interesting.
|
||||
</p><p>
|
||||
I'm curious to go back, in a sense, to read those struggles to make sense of Nazism from the outside -- Neumann was inside-outside, because he was an émigré – as an inspiration for thinking in similarly open-ended and protean terms about China in the present. Not as a template, but as an inspiration.
|
||||
</p><p>
|
||||
There’s nothing I could read in the West right now that makes me feel I fully understand the dynamics. Looking at sectors seems to help. In energy, for example. We can go quite a long way using openly available Chinese sources to understand the dynamic of competition between the coal people and the big oil companies and the new set of interests associated with the renewable energy industry. That provides us with a keyhole for understanding certain dynamics.
|
||||
</p><p>
|
||||
There are certain lessons that I think we can learn, in the sense that if we can't observe them, at least there are certain biases on our own part that we can check. There are certain common habits of thought that I see recurring in American efforts to make sense of China. We saw them in a kneejerk form at the beginning of this year, the Chernobyl moment analogy that rapidly surfaced. Of all the analogies in the world, it strikes me as truly bizarre. Does Xi’s regime now look like the Soviet Union in 1986? I don't think so. Nevertheless, it was very compelling -- and apparently quite compelling to people inside China as well, who were using the film review websites to leave comments. I think we need to be very conscious of those sorts of impulses in our own mindset and try to read against the grain of our own desire to see certain neat conclusions and certainly, root storylines acted out.
|
||||
</p><p>
|
||||
<strong>Jordan</strong>: <a href="https://twitter.com/You_Shu_China">Youshu</a>, the anonymous guy who wrote <a href="https://web.archive.org/web/20200130074904/http://credibletarget.net/notes/Germanecon">that review of</a> <em><a href="https://web.archive.org/web/20200130074904/http://credibletarget.net/notes/Germanecon">Wages of Destruction</a></em>, sent me this question: You argue Hitler went to war with an inadequate industrial base and people assume that Xi won't attempt to take over Taiwan until he's confident of victory. Given what you know about the incentives facing Hitler, do you think there's anything we can learn about how Xi is thinking about Taiwan?
|
||||
</p><p>
|
||||
<strong>Adam</strong>: I think the much closer analogy is to Stalin. This is the point made by the vastly underrated conservative historian <a href="https://en.wikipedia.org/wiki/Andreas_Hillgruber">Andreas Hillgruber</a> in his magnificent global history of strategy in the first phase of the war. There's a huge difference between Hitler and Stalin, because Stalin believes confidently that time is on his side. The time horizon is everything in deciding whether or not the strategic window is open and how long it's open for. Whereas Hitler is gnostic, literally, in a kind of theological sense. Hitler has no confidence at all that time is on his side and he knows that, from an economic point of view, the cards are indeed stacked against him. The advantage that the Germans built up early on in the arms race is probably going to dwindle. Furthermore, he is possessed by the idea that the conflict is inescapable, that the world Jewish conspiracy is pressing in upon Germany whether in the form of an actual hot war or whether by means of the cold war pressures that he believes are the explanation for Germany's economic problems.
|
||||
</p><p>
|
||||
There is for him a way of reading Germany’s dwindling foreign currency reserves as an index of the success of the Jewish effort to strangle the German economy. You see this manifestly in <a href="https://en.wikisource.org/wiki/Adolf_Hitler%27s_Address_to_the_Reichstag_(30_January_1939)">his notorious speech on the 30th of January, 1939</a>, where he actually threatens that if world war should break out – which in German terms means a war involving the British empire and the United States – then it will be the Jews of Europe that pay. This is the moment when he publicly announces the Holocaust. The rest of that speech is all taken up with economic problems and shortages of foreign exchange. It's extraordinary.
|
||||
</p><img src="https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F06357918-4222-4958-a1dc-c626a1a64f28_1280x920.png" alt="File:Adolf Hitler's speech in the Reichstag, 30 January 1939.png - Wikimedia Commons"/><p>
|
||||
I don't see that level of time pressure and anxiety. I know people say that the Chinese think that time is not on their sides either, that in the end they're going to be overtaken by India, that their population is aging. But the time clock runs over decades. That wasn't true for somebody like Hitler. This suggests that the Chinese situation is radically different.
|
||||
</p><p>
|
||||
<strong>Jordan</strong>: It's interesting thinking about the extreme Republican take on U.S.- China relations, how it almost echoes the clock running out on the U.S. We have to shut down Huawei, we have to take down SMIC, because time is absolutely not on our side.
|
||||
</p><p>
|
||||
<strong>Adam</strong>: The answer to this apocalyptic thinking, which in the case of folks like Pompeo who belong to the evangelical faction around the Trump administration, they really do have a kind of end times possibility in their kind of cosmology. That's a dangerous thought pattern. Apparently, Peter Navarro sincerely believes that there is the possibility of a shooting war with China as a realistic prospect over the next couple of decades. That’s dangerous stuff, because the job is not to refuse a relative decline, because that's necessary and legitimate and inevitable. The question is to code it as something other than end times.
|
||||
</p><p>
|
||||
The farcical element amidst the German tragedy is that in, 1945, they are looking around them and expecting the final historic erasure of Germany. We know that's absolutely not what happened, and it never needed to have happened.
|
||||
</p><p>
|
||||
<a href="https://en.wikipedia.org/wiki/Konrad_Adenauer">Konrad Adenauer</a> is older than <a href="https://en.wikipedia.org/wiki/Gustav_Stresemann">Gustav Stresemann</a> and presided over the resurrection of a Germany that turns out to be absolutely essential to the European balance of power, the European economy, and everyone welcomes back and makes rich. This sort of apocalyptic thinking of a history that's going to end with some sort of big bang, or some terrible ghastly discreditable whimper, rather than just facing up to reality in which the world is different and America's position is not what it was in 1945. Which is not after all the end of the world.
|
||||
</p><p>
|
||||
The worry for me is more about the American side than it is about China. This is in no way to diminish the aggression that Beijing is clearly displaying, it's determination to resolve the “Hong Kong problem,” and its very earnest commitment to finding a new solution for Taiwan in the foreseeable future. This isn't to underestimate the seriousness of that intent on their part. But I don't think it's driven by the insistent ticking clock kind of logic.
|
||||
</p><h1>
|
||||
Why Chinese Political Theorists Are Drawing on Nazi Germany’s Favorite Legal Thinker
|
||||
</h1><p>
|
||||
<strong>Jordan</strong>: So, who's the German legal theorist Schmitt, and why is he having a bit of a <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3400946">renaissance</a> nearly a hundred years later in China today.
|
||||
</p><p>
|
||||
<strong>Adam</strong>: It's one of the truly remarkable the facts about the last 15 to 20 years of Chinese political thinking. It’s worth saying there are deeply intelligent, obviously brilliant political and legal theorists working in China, trying to articulate and make sense of the logic of this emerging power. One of the sources that they go to – and this has emerged from the tireless work of many translators of recent Chinese political writing and international relations thought is -- is a German political and legal theorist called Carl Schmitt.
|
||||
</p><p>
|
||||
He really had his heyday in the Weimar Republic and then was one of the key legal minds behind the early history of the Third Reich. He provided one of the chief legal justifications for the overthrow of the Prussian government, which was a prelude to the Nazi seizure of power at the national level in Germany. He went on to be one of the most influential legal theorists of the Third Reich in the 1930s. Apart from his alignment with the party, which is true of many German intellectuals in the period, he was also a flaming Anti-Semite. He would label colleagues with the name Judo and attaching yellow stars to bibliographies.
|
||||
</p><p>
|
||||
Lived on deep into the early 1980s, he was almost one hundred and really just wouldn't die. He became a kind of pivotal influence on German legal thought even after 1945. He ran a sort of secret seminar and has been quite influential on German legal thought since.
|
||||
</p><p>
|
||||
Why? <strong>Because he has one of the most hard-nosed answers to liberalism. He insists upon understanding politics as a distinction between friend and foe, friend and enemy. He insists that legal orders have real foundations in space and in power, in the taking of a territory, fundamentally.</strong> That they therefore have limits and necessarily have limits, that they define insiders and outsiders. That structures which are truly comprehensive threatened to emerge in the course of the 20th century. He's one of the people that was very influential on me in writing <em>The Deluge</em> because he understood the order that emerged out of the end of World War One as a unipolar order, a kind of universal empire defined by the United States. But not simply as a peaceful empire, but as essentially the instantiation of the overweening power of America, the British empire, business. He was one of the most coruscating and effective critics of the way in which liberalism disguises its power in morality and the logic of commerce.
|
||||
</p><img src="https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F797bb996-60a4-465f-8870-446c054c95bb_1200x675.png" alt=""/><p>
|
||||
Having laid it out like that I think you can see why call Schmitt has been attractive, not just for Chinese legal scholars, but for a variety of scholars both in Europe and in the United States. There is a live active Schmittianism in Harvard Law School around the figure of <a href="https://en.wikipedia.org/wiki/Adrian_Vermeule">Adrian Vermeule</a>, who's arguing that to understand the actions of the American government since 9/11, you have to be willing to understand the legitimate role for executive power in defining the enemies of the state and going after them. He is one of the people who justified the use of torture after 9/11. But, also, the emergency powers invoked by Ben Bernanke in 2008. You do that against the backdrop of a set of political commitments. This is the sort of moment of here I am, and I can do no other. This is my politics, and this is the political and legal regime that instantiates that.
|
||||
</p><p>
|
||||
The left in Europe uses this kind of critique as a way of unpicking the hypocrisy of liberal internationalism in the age of Clinton and the responsibility to protect and that discourse of global internationalism which, they, the left, read, as the updated version of 1920s internationalism. For all of these reasons I think you can immediately see why Chinese scholars seized on precisely this logic for thinking through the emergence of Chinese power in the context of what they would diagnose as American empire.
|
||||
</p><p>
|
||||
The first use you can make of Schmitt is to assert the necessity of politics and the absolute necessity of committing yourself to the project, some project. The project they commit themselves to is the project of the CCP. And from that law follows, not the other way around. You don't have a legal system and then as it were defined the party within it. In fact, the appropriate way around is to define your norms and then follow a legal system from that. The norms you pick is up to you. It could be liberal, it could be those of the Communists.
|
||||
</p><p>
|
||||
But also, in a more ironic twist, they also see in Schmitt a theorist of China's own potential empire, a zone of influence and power that would create its own structures of incorporation, its own norms. Given from China, defined by China, set and ultimately rooted in Chinese power, radiating out from East Asia, encompassing other states.
|
||||
</p><p>
|
||||
This, too, was Schmitt’s vision, that on this basis you could found an empire. It's worth saying that Schmitt is a failed figure. He starts out well in the Third Reich. He’s a passionate advocate of the Hitler impact, which conforms precisely to his sort of vision of the world divided up between, conceivably, the Germans sphere, the Soviet sphere, the Japanese sphere, and the United States-British empire sphere. And all of that of course collapses between 1940 and 1941, when it becomes obvious that Hitler’s project is not actually compatible well with that kind of stable vision of the partition of the world into separate zones.
|
||||
</p><p>
|
||||
For the Schmittians in China right now, that's the question, too. In a sense, “One Nation, Two Systems” was precisely a kind of Schmittian vision of an overarching Chinese empire, with the pluralism that's contained within that. Safely contained with the acknowledgement that this is about China, under the leadership and the hegemony of the CCP, tolerating two systems. Of course, that isn't what we're seeing.
|
||||
</p><p>
|
||||
<strong>Jordan</strong>: But the end of the day, there are other logics that are more powerful than his logic, so you see Hong Kong, Xinjiang and Tibet going they way they are.
|
||||
</p><h1>
|
||||
Xinjiang and Soviet Gulags
|
||||
</h1><p>
|
||||
<strong>Jordan</strong>: Any thoughts on the evolving Nazi rationale for labor camps and Xinjiang?
|
||||
</p><p>
|
||||
<strong>Adam</strong>: <strong>Xinjiang seems to fit much more squarely in the logic of sort of the Soviet Gulag system</strong>, that it does in this sort of exterminatory logic of national socialism. There is an element of the Nazi concentration camps system which is a forced labor camp system. But it was always ultimately genocidal in the literal physical sense. I don't think there's every reason to think that the Chinese project of cultural transformation properly meets genocidal standards in Xinjiang. It goes a very long way in that direction. But there's no necessity for destruction through labor, which was understood by the Nazis and an absolutely core component of their incarceration and murder of the Jewish population, and also very large slices of the Slavic population of Eastern Europe as well.
|
||||
</p><p>
|
||||
I don't see that logic being analogous. The colonial logic, the settlement logic, the use of forced labor for the purposes of political reeducation and also infrastructural transformation. The history, after all, of the Xinjiang forced labor complex and the state-owned enterprises there goes all the way back at least to the Cultural Revolution, if not the late fifties. But, again, I would caution against wanting to subsume China into someone else's archetype of what a forced labor regime or a genocidal regime looks like. It seems to me that there could be a horrifying originality to what they're doing. After all the tech dimension of what China's able to do now in terms of surveillance is beyond the wildest dreams of any previous authoritarian regime.
|
||||
</p><p>
|
||||
<strong>Matt</strong>: The apparent desire of the local government in Xinjiang to reduce the birth rate of the Uighur population complicates the story of how we would characterize this one way or the other.
|
||||
</p><p>
|
||||
<strong>Adam</strong>: It too easily slides out of consciousness that the Chinese regime undertook what Foucauldians would call one of the most grotesque, grandiose, and very violent political experiments in history, the one-child policy. It pursued that towards the Han population. That it’s then also capable of taking those kinds of techniques and applying them to resistant population like the Uighurs or Tibetans or the Mongolians, I don't think it’s surprising. It's the same toolkit. In the eighties, it was ferocious in its intrusiveness, monitoring women's menstrual cycles, forced abortions. This stands alone. No one's ever done that before on that scale. It’s quite mind-blowing and too easily consigned to the history books.
|
||||
</p><img src="https://cdn.substack.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fac9cc6de-22ae-4c2a-a45c-a4a57b7bfcbb_2880x1631.png" alt=""/><p>
|
||||
<strong>Jordan</strong>: This is a technocratic regime deciding that this is the best thing and then getting a whole bureaucracy and apparatus to buy into it.
|
||||
</p><p>
|
||||
<strong>Adam</strong>: Some people tell us it's driven by <a href="https://en.wikipedia.org/wiki/Song_Jian">a bunch of people from the rocket aeronautics program</a> and people who came out of Western-inspired cybernetics thinking, basically taking the limits to growth diagnosis too, literally. So, I completely agree that element of the repressive apparatus in Xinjiang is really dramatic and fully consistent with an indictment of genocide.
|
||||
</p><p>
|
||||
This is one of the real puzzles about COVID, right? This is a regime, which in the name of socialist transformation shrimps from nothing, has also been astonishingly effective in its approach to the virus. Perhaps the common denominator is simply control and this being a matter of the highest possible political stakes. Not to be able to control this virus would be a far more serious blow to the prestige and legitimacy of a regime which has those kinds of pretensions than it is to the Trump administration in the U.S., which in the end just shrugs.
|
||||
</p><h1>
|
||||
Did The West Win The Cold War?
|
||||
</h1><p>
|
||||
<strong>Matt</strong>: Adam, <a href="https://www.lrb.co.uk/the-paper/v42/n15/adam-tooze/whose-century">you recently wrote a really interesting essay</a> for the London <em>Review of Books</em> in which, among other things, you argued that the U.S. lost the Cold War in Asia, and that taking a long view of the past, that it wasn't really a situation where American interests were advanced, even though in principle, it seemed like at the time we'd won with the collapse of the USSR and the expansion of NATO. What would winning in your view of look like relative to what actually occurred? To what extent would the emergence of China under an undemocratic government inevitably have led there, in the same way that the U.S. and the USSR came into a hostile post-War relationship?
|
||||
</p><p>
|
||||
<strong>Adam</strong>: What it is that we mean when we anchor our understanding of late 20th century history in the idea that the West won the Cold War? In Europe, that looks like the collapse of the Soviet Union, the disintegration of the Warsaw pact, the incorporation of the former Warsaw pact States into the EU and NATO. That's what victory looks like. That is the result of a culmination of several decades of crisis in Eastern Europe up to, and including, the imposition of martial law in Poland.
|
||||
</p><p>
|
||||
None of that happened in East Asia. We were fought to a stalemate in Korea. Vietnam was a debacle. And one of the key anchors of the ultimate demise of Soviet Union is, after all, an alliance with China. And in 1989 at the moment, as it were, where the chips are falling and the Warsaw pact is disintegrating in Eastern Europe, we have Tiananmen Square, the Communist Party basically giving notice that this regime change will not extend to them.
|
||||
</p><p>
|
||||
So that's what I was gesturing towards. We are really dealing with a Communist Party that's alive and kicking. Not of course as it were the Communist Party of Mao, but nevertheless an heir descendant of that party, is what we have to get to grips with and come to terms with. I think that ought to force us to reconsider this notion that the Cold War ended with us winning as it did in Europe. It didn't in Asia, and Korea feels the force of that, Japan feels the force of that, and the United States is now coming to terms with it too.
|
||||
</p><h1>
|
||||
Favorite Bureaucracies To Work For
|
||||
</h1><p>
|
||||
<strong>Jordan</strong>: I have a lighter one for you. Adam, pick three bureaucracies in particular time spans in which you would most like to be an employee of.
|
||||
</p><p>
|
||||
<strong>Adam</strong>: What a wonderful question! I would like to have been in the Prussian state in 1806 and to have witnessed the shock of Napoleon, and the recovery and the emergence of the moment of German idealism, and the arrival of Hegel in Berlin, and the arrival of Clausewitz. As a Brit, somebody interested in economics, I want to be in the treasury in the UK any moment that Keynes is around, could be during World War One, could be during World War Two. To see a bureaucracy that in some respects is the ideal type of a high functioning machine. If you read the reports from Versailles, the Brits are effectively the people running the show because the civil service was a magnificent, bureaucratic machine. And then to see it inspired by a genius like that, you know what I mean?
|
||||
</p><p>
|
||||
It’s parochial, but it would have been pretty interesting to be in Bonn in 1989 and to have seen that moment from the inside. And some of those people are majorly underrated players. And we know now the end of the Cold War is a multipolar business. But I think as somebody who grew up in West Germany, it would have been fantastically interesting to track that moment from the inside.
|
||||
</p><h1>
|
||||
<a href="https://glow.fm/chinatalk">Please consider supporting ChinaTalk.</a>
|
||||
</h1><h3>
|
||||
I’m also on the job market if anyone has leads on full time or consulting gigs!
|
||||
</h3></div>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<div><h1>
|
||||
The Great Eurasian Economic Realignment
|
||||
</h1><img src="/sites/cj/themes/cj/images/top.svg" alt="back to top"/><picture><source srcset="https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!450,450/0/default.jpg 450w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!900,900/0/default.jpg 900w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!952,952/0/default.jpg 952w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!1904,1904/0/default.jpg 1904w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!1900,1900/0/default.jpg 1900w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!3800,3800/0/default.jpg 3800w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!2500,2500/0/default.jpg 2500w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!5000,5000/0/default.jpg 5000w ," sizes="100vw"/> <img src="https://media4.manhattan-institute.org/sites/cj/files/will-western-sanctions-accelerate-russian-realignment.jpg" srcset="https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!450,450/0/default.jpg 450w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!900,900/0/default.jpg 900w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!952,952/0/default.jpg 952w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!1904,1904/0/default.jpg 1904w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!1900,1900/0/default.jpg 1900w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!3800,3800/0/default.jpg 3800w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!2500,2500/0/default.jpg 2500w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fwill-western-sanctions-accelerate-russian-realignment.jpg/full/!5000,5000/0/default.jpg 5000w ," sizes="100vw" alt="" title=""/></picture><div>
|
||||
<span>eye on the news</span>
|
||||
</div><h2>
|
||||
The Great Eurasian Economic Realignment
|
||||
</h2><div><span>Western sanctions may accelerate Russia’s economic integration with Asia.</span>
|
||||
<span>March 17, 2022</span>
|
||||
</div><div>
|
||||
<span>Politics and law</span>
|
||||
</div><div>
|
||||
<span>Economy, finance, and budgets</span>
|
||||
</div><p>
|
||||
Since Vladimir Putin ordered the Russian Armed Forces to <a href="https://www.city-journal.org/putins-bet" target="_blank">launch a full-scale invasion</a> of Ukraine on February 24, Western countries and their close allies have hit Russia with a raft of sweeping and unprecedented <a href="https://www.city-journal.org/under-heavy-sanctions-time-is-not-on-russias-side" target="_blank">sanctions</a>. Several major Russian banks were cut off from the Belgium-based SWIFT international financial messaging system. Transactions with Russia’s central bank were banned by the United States, United Kingdom, and European Union, effectively preventing Russia from utilizing much of its $640 billion of foreign exchange reserves. Targeted asset freezes and seizures were carried out against high-level members of Russia’s political and economic elite, Russian planes were denied access to essentially all European and North American airspace, and an unofficial boycott of Russia has seen <a href="https://www.dw.com/en/which-companies-have-pulled-out-of-russia-over-the-ukraine-invasion/a-61078955" target="_blank">over 300</a> international businesses suspend operations in or with the country, including major brands such as Apple, Disney, Zara, Visa, IKEA, and Coca-Cola. The Russian ruble has <a href="https://www.nytimes.com/2022/03/09/business/russia-ruble-central-bank.html" target="_blank">lost</a> about 40 percent of its value against the U.S. dollar in a matter of weeks.
|
||||
</p><p>
|
||||
This kind of economic warfare is designed to isolate Putin’s government and bring an end to the invasion. But Russia’s vast exports of oil, coal, and natural gas to Europe have been notably exempt from the sanctions so far. Europe heavily relies on Russia to meet its energy needs: Russia alone <a href="https://edition.cnn.com/2022/03/08/energy/gas-russia-europe/index.html" target="_blank">supplies</a> 40 percent of the European Union’s natural gas imports, 46 percent of EU coal imports, and 27 percent of EU oil imports. Russia is also the world’s largest <a href="https://www.aljazeera.com/news/2022/2/17/infographic-russia-ukraine-and-the-global-wheat-supply-interactive" target="_blank">wheat</a> exporter, a leading producer of fertilizer, and possesses vast mineral reserves. The sanctions and boycotts against Russia are thus perhaps better termed financial warfare. Despite the belief that financial pain for the Russian populace will translate into political change, the nature of the Russian government remains the same. And while such bureaucratic and accounting moves can certainly cause significant pain in the short and medium term, they cannot change the fact that Russia possesses valuable and useful natural resources.
|
||||
</p><p>
|
||||
But as the <a href="https://www.forbes.com/sites/rrapier/2021/11/14/is-the-us-energy-independent/" target="_blank">energy-independent U.S.</a> pushes for energy sanctions, Germany in particular finds itself in an increasingly difficult position. For decades, Germany’s political and business elites have been <a href="https://brief.bismarckanalysis.com/p/the-german-retreat-from-nuclear-power" target="_blank">implementing a wide-ranging transition</a> from reliance on fossil fuels and nuclear energy to wind, solar, and biofuels. This energy transition, called the <i>Energiewende</i> in German, was always a precariously ambitious strategy for Europe’s premier industrial and manufacturing power. Heavy industry requires consistent and abundant energy—something that <a href="https://brief.bismarckanalysis.com/p/photovoltaics-give-credibility-to" target="_blank">photovoltaic solar panels could provide in theory</a> but not in dim and cloudy Germany. With its nuclear plants now almost entirely shuttered, Germany’s energy transition has instead resulted in a reliance on Russian energy significantly higher than the EU average, with <a href="https://www.bp.com/content/dam/bp/business-sites/en/global/corporate/pdfs/energy-economics/statistical-review/bp-stats-review-2021-natural-gas.pdf" target="_blank">55 percent of natural gas imports</a> coming from Russia in 2020.
|
||||
</p><p>
|
||||
Will that change? In the wake of Russia’s invasion of Ukraine, the German government briefly floated the possibility of delaying shutdowns of its few remaining nuclear plants, before ultimately <a href="https://www.reuters.com/world/europe/german-ministries-say-cannot-recommend-extending-nuclear-plants-lifetime-2022-03-08/" target="_blank">deciding against it</a> and instead emphasizing plans to increase liquefied natural gas (LNG) imports and development of renewables. While the U.S. has announced plans to ban Russian energy imports immediately, new European <a href="https://edition.cnn.com/2022/03/08/energy/gas-russia-europe/index.html" target="_blank">plans</a> to reduce energy imports from Russia are less ambitious on paper but could prove harder to achieve.
|
||||
</p><p>
|
||||
Political considerations may ultimately outweigh economic ones in Europe. All that is stopping European countries from eventually reducing energy imports from Russia to zero is their willingness to pay the costs of higher energy import prices, such as by importing LNG from the United States, the Middle East, or Africa, or by continuing to subsidize the construction and operation of renewable-energy installations. Much as Westerners overrate the effects of economic hardship on political outcomes in Russia, Iran, or North Korea, it would be a mistake for Russia to overrate the effects of economic hardship on political outcomes in Europe. For while Russia is Europe’s largest energy supplier, Europe is Russia’s largest trade partner. The complete loss of this trade relationship would have far harsher impacts on Russia’s economy and state coffers than even the current round of financial sanctions.
|
||||
</p><p>
|
||||
What has not been widely considered, however, is the possibility that Russia welcomes this outcome. If Russia is betting on economic divorce from Europe, including in energy, then sanctions and boycotts counterintuitively support, rather than frustrate, Russian strategy. Indeed, Putin’s government is already planning to retaliate with economic warfare of its own: it has prepared its own list of “unfriendly” countries to which to <a href="https://www.bloomberg.com/news/articles/2022-03-08/russia-to-restrict-some-raw-material-exports-but-omits-details" target="_blank">ban exports</a> of natural resources, and has <a href="https://www.bloomberg.com/news/articles/2022-03-07/russia-threatens-to-cut-gas-flows-to-europe-via-nord-stream-1" target="_blank">threatened</a> to shut down gas to Germany via the Nord Stream 1 pipeline in retaliation for Germany halting the certification of Nord Stream 2.
|
||||
</p><p>
|
||||
Perhaps these moves can be written off as symbolic reactions, but they may fit a broader strategy of replacing Europe with Asia as Russia’s key customer for natural resources. Russia has been heavily investing in development and infrastructure <a href="https://brief.bismarckanalysis.com/p/russias-long-term-bet-on-the-arctic" target="_blank">in its Arctic and subarctic regions</a>, which produce an overwhelming majority of its fossil fuels and a substantial portion of its valuable minerals. The Russian government has gone so far as to plan a “<a href="https://en.wikipedia.org/wiki/Northern_Sea_Route" target="_blank">Northern Sea Route</a>” to Asia across Russia’s vast Arctic coastline, normally considered far too icy and dangerous for regular maritime shipping. Such a route could prove geopolitically vital, even if uneconomical, in case Russian ships are denied access to Western ports, as the United Kingdom <a href="https://www.reuters.com/world/uk/uk-bans-russian-ships-docking-its-ports-2022-02-28/" target="_blank">has already done</a>. To this end, Russia has also revived its fleet of nuclear-powered icebreakers and built the world’s first floating nuclear power station to supply electricity to remote regions of Siberia, with more of each on the way.
|
||||
</p><p>
|
||||
Russia already depends on natural resource exports. That won’t change. But over the coming decades, it will not be the stagnant economies of Europe that will have the greatest demand for energy and minerals. Rather, the maturing industrial economy of China, in addition to the still-industrializing economies of India and other Asian and African countries, will be the primary growth markets for Russia’s natural resources. Neither China, India, Indonesia, the Gulf States, nor any of the many countries in Africa and Latin America chose to <a href="https://en.wikipedia.org/wiki/International_sanctions_during_the_Russo-Ukrainian_War" target="_blank">sanction</a> Russia over the invasion of Ukraine. Today’s economic disassociation may well have unintended consequences for Europe.
|
||||
</p><p>
|
||||
<strong>Photo by ALEXEY DRUZHININ/SPUTNIK/AFP via Getty Images</strong>
|
||||
</p><div>
|
||||
<span><em>City Journal</em> is a publication of the Manhattan Institute for Policy Research (MI), a leading free-market think tank. Are you interested in supporting the magazine? As a 501(c)(3) nonprofit, donations in support of MI and <em>City Journal</em> are fully tax-deductible as provided by law (EIN #13-2912529).</span> <a href="/donate?p=will-western-sanctions-accelerate-russian-realignment">DONATE</a>
|
||||
</div><div>
|
||||
<span>More from</span>
|
||||
|
||||
</div><picture><source srcset="https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!519,173/0/default.jpg 519w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!1038,346/0/default.jpg 1038w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!649,200/0/default.jpg 649w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!1298,400/0/default.jpg 1298w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!461,200/0/default.jpg 461w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!922,400/0/default.jpg 922w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!500,200/0/default.jpg 500w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!1000,400/0/default.jpg 1000w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!334,200/0/default.jpg 334w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!668,400/0/default.jpg 668w ," sizes="(max-width: 519px) 100vw, (max-width: 649px) 100vw, (max-width: 952px) 461px, (max-width: 1139px) 500px, 334px"/> <img src="https://media4.manhattan-institute.org/sites/cj/files/destroyed-military-facility-Brovary-outside-Kyiv.jpg" srcset="https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!519,173/0/default.jpg 519w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!1038,346/0/default.jpg 1038w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!649,200/0/default.jpg 649w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!1298,400/0/default.jpg 1298w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!461,200/0/default.jpg 461w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!922,400/0/default.jpg 922w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!500,200/0/default.jpg 500w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!1000,400/0/default.jpg 1000w , https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!334,200/0/default.jpg 334w ,https://media5.manhattan-institute.org/iiif/2/sites%2Fcj%2Ffiles%2Fdestroyed-military-facility-Brovary-outside-Kyiv.jpg/full/!668,400/0/default.jpg 668w ," sizes="(max-width: 519px) 100vw, (max-width: 649px) 100vw, (max-width: 952px) 461px, (max-width: 1139px) 500px, 334px" alt="" title=""/></picture></div>
|
||||
46
packages/readabilityjs/test/test-pages/cnbc/distiller.html
Normal file
46
packages/readabilityjs/test/test-pages/cnbc/distiller.html
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<div><div>
|
||||
<time>Published Mon, Jul 24 20171:56 PM EDT</time>
|
||||
</div><div>
|
||||
<span>Share</span></div><div>
|
||||
<span>0:32</span>
|
||||
</div><div>
|
||||
George Clooney just sold his tequila business for up to $1 billion
|
||||
</div><p>
|
||||
In celebration of National Tequila Day on July 24, George Clooney may be toasting to <a href="http://casamigostequila.com/" target="_blank">Casamigos Tequila</a>, which the Hollywood star co-founded with friend Rande Gerber.
|
||||
</p><p>
|
||||
The company, which the duo recently sold to <a href="http://data.cnbc.com/quotes/DGE-GB" target="_blank">Diageo</a> for <a href="https://www.cnbc.com/2017/06/21/george-clooney-just-sold-his-tequila-business-for-1-billion.html">as much as $1 billion</a>, was never meant for the public. It started out as a private collection of tequila for the co-founders and their friends and family.
|
||||
</p><p>
|
||||
While building their side-by-side vacation homes in Cabo San Lucas, the long-time friends were spending a lot of time in Mexico and, consequently, drinking a lot of tequila.
|
||||
</p><p>
|
||||
“As you do when you’re in Mexico, you drink a lot of tequila,” Gerber tells CNBC. “After a couple of months trying a lot of different tequilas, George turned to me and said, ‘Why don’t we just make our own? One that’s perfect for us to drink?’”
|
||||
</p><p>
|
||||
That’s just what the pair did, along with their friend Michael Meldman.
|
||||
</p><p>
|
||||
The team found a distiller in Jalisco, Mexico, and explained exactly what they were looking for. “It was important that it tasted great and didn’t burn going down,” Gerber explains. “We wanted one that we could drink straight or on the rocks. One that we could drink all day and all night, and not be hungover in the morning.”
|
||||
</p><picture><source srcset="https://image.cnbcfm.com/api/v1/image/104073358-cheers_1.jpg?v=1529473142&w=740&h=416" media="(min-width: 1340px)" src="https://image.cnbcfm.com/api/v1/image/104073358-cheers_1.jpg?v=1529473142&w=740&h=416"/><source srcset="https://image.cnbcfm.com/api/v1/image/104073358-cheers_1.jpg?v=1529473142&w=630&h=354" media="(min-width: 1020px)" src="https://image.cnbcfm.com/api/v1/image/104073358-cheers_1.jpg?v=1529473142&w=630&h=354"/><source srcset="https://image.cnbcfm.com/api/v1/image/104073358-cheers_1.jpg?v=1529473142&w=900&h=506" media="(min-width: 760px)" src="https://image.cnbcfm.com/api/v1/image/104073358-cheers_1.jpg?v=1529473142&w=900&h=506"/><img src="https://image.cnbcfm.com/api/v1/image/104073358-cheers_1.jpg?v=1529473142&w=678&h=381" alt="The Casamigos co-founders (from left to right): Michael Meldman, George Clooney, and Rande Gerber."/></picture><div>
|
||||
The Casamigos co-founders (from left to right): Michael Meldman, George Clooney, and Rande Gerber.
|
||||
</div><div>
|
||||
Source: Casamigos Tequila
|
||||
</div><p>
|
||||
After months of back-and-forth with the distiller, and tasting and developing their tequila, the duo found the perfect product, Gerber recalls: “George opened the sample, poured one for me, poured one for him, and we tasted it. We both looked at each other, had another taste of it, and we were like, ‘This is it. It’s perfect.’”
|
||||
</p><p>
|
||||
Casamigos, which loosely translates to “house of friends,” was never meant for the public. For two years, it was served exclusively among Gerber and Clooney’s friends and family.
|
||||
</p><p>
|
||||
“It was just for us,” Gerber says. “We didn’t really want to be in the business. We figured, George is an actor and a director. I own restaurants and bars and wasn’t looking to get in another business.
|
||||
</p><p>
|
||||
“But then the distiller called and said, ‘Hey guys, we have a little problem: In the past two years, we’ve been sending you about a thousand bottles a year. Either you’re selling it or you’re drinking way too much — either way, we can’t keep calling it samples. You guys have to get licensed and do this right.’”
|
||||
</p><p>
|
||||
The celebrity entrepreneurs, who brought Casamigos to the public in 2013, figured they didn’t have a choice. “We wanted to keep drinking it,” Gerber says.
|
||||
</p><picture><source srcset="https://image.cnbcfm.com/api/v1/image/104073400-GC_and_RG_Summer_2015.jpg?v=1529473143&w=740&h=416" media="(min-width: 1340px)" src="https://image.cnbcfm.com/api/v1/image/104073400-GC_and_RG_Summer_2015.jpg?v=1529473143&w=740&h=416"/><source srcset="https://image.cnbcfm.com/api/v1/image/104073400-GC_and_RG_Summer_2015.jpg?v=1529473143&w=630&h=354" media="(min-width: 1020px)" src="https://image.cnbcfm.com/api/v1/image/104073400-GC_and_RG_Summer_2015.jpg?v=1529473143&w=630&h=354"/><source srcset="https://image.cnbcfm.com/api/v1/image/104073400-GC_and_RG_Summer_2015.jpg?v=1529473143&w=900&h=506" media="(min-width: 760px)" src="https://image.cnbcfm.com/api/v1/image/104073400-GC_and_RG_Summer_2015.jpg?v=1529473143&w=900&h=506"/><img src="https://image.cnbcfm.com/api/v1/image/104073400-GC_and_RG_Summer_2015.jpg?v=1529473143&w=678&h=381" alt="George Clooney and Rande Gerber."/></picture><div>
|
||||
George Clooney and Rande Gerber.
|
||||
</div><div>
|
||||
Source: Casamigos Tequila
|
||||
</div><p>
|
||||
The organic evolution of Casamigos made the process of starting a company less stressful, Gerber tells CNBC: “This all happened naturally. We just wanted the best tequila for us to drink — so when it turned into a business, it was easy, because we didn’t really care about whether or not it was a huge success or not.”
|
||||
</p><p>
|
||||
What started as a passion project turned into a successful private-label tequila business. “It immediately took off,” says Gerber, and now, Diageo will initially pay $700 million for it, with the potential for another $300 million depending on the tequila’s performance.
|
||||
</p><p>
|
||||
The key to their success has been focusing on the product <em>inside</em> the bottle, not the bottle or label itself, says Gerber. “We have an inexpensive bottle, but the best quality tequila inside.”
|
||||
</p><p>
|
||||
<em>This is an updated version of a story that was previously published in November 2016.</em>
|
||||
</p></div>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<div><div>
|
||||
I have a regular small sound job that I handle with an XR12 controlled from a 10" Android tablet running Mixing Station X Air Pro. The only challenge with this job is that I get very little time for get-in and get-out so any problems in setting up could be disastrous. I carry a spare 7" tablet, backup access point and an OTG adaptor for wired Ethernet but I don't have much time for trouble-shooting.<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
I have long thought it would be good to have basic control via MIDI in case I lost my normal user interface but I did not want a large and/or expensive controller. I did want the controller to have feedback for mute states and fader levels. I have achieved this using the XTouch Mini.<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
The XTouch Mini, in common with most small and reasonably priced controllers, is a USB MIDI device and the XR12 requires DIN MIDI. This can be resolved, though, with a USB/DIN MIDI converter. The <i>Kenton MIDI USB Host</i> would work but is rather expensive. The <i>HobbyTronics MIDI USB-DIN Converter</i> is much lower cost but looks a little fragile (*) so would need to be repackaged for life on the road. I took the low-cost route by using <i>HobbyTronics USB Host Mini (preloaded with MIDI firmware)</i> plus a <i>HobbyTronics MIDI Breakout</i>. These two boards are mounted in a small box along with a connector for 5Vdc power. The converter and controller only draw about 100mA so I can run it for over twelve hours from a 2000mAh phone powerbank if there is no convenient power outlet or spare USB-A port.<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
Using the <i>XTouch Editor</i>, I have programmed eight encoders and eight buttons to control the faders and mutes for the critical channels and buses using the published MIDI messages. The encoder LED rings and mute LEDs follow the XR state when controlled by both the MIDI board and the normal network UI. When first turned on, though, the XTouch does not reflect the state of the XR. The quick solution to this is to load an XR snapshot which gives instant synchronization so I programmed one of the spare XTouch buttons to load my default snapshot. As another handy shortcut, I use the encoder press functions to set default fader levels for their respective channels/buses. This is another way to get the faders synchronized without loading a snapshot.<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
So I have achieved what I set out to do: I can take control of the basic mute and fader functions of the XR if my tablet and/or network fail. I hope that I don't need this capability but it's in the mixer's gig-bag just in case.<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
(*) HobbyTronics have improved this product. It should not now need repackaging. See <a href="http://www.hobbytronics.co.uk/usb-midi-converter?utm_source=google&utm_medium=googleshopping&utm_campaign=googlebase&gclid=EAIaIQobChMIo_y2jPnX2wIVjr_tCh0ryghCEAQYAyABEgIejvD_BwE" target="_blank" rel="nofollow noopener noreferrer">HobbyTronics USB/Midi Converter.</a><a href="/public/ses-discussion-attachment/XTouch%20Mini.jpg">XTouch Mini.jpg</a><a href="/public/ses-discussion-attachment/Inside.jpg">Inside.jpg</a><a href="/public/ses-discussion-attachment/Converter.jpg">Converter.jpg</a>
|
||||
</div><div>
|
||||
<span title="11,159"> <span>11,159</span></span>
|
||||
</div><img src="/application/modules/Mtcore/externals/images/loading.gif"/><ul><img src="/public/core_file/9a/5c/02/09fef942345ef4e840dc4c9f78cffc3e.svg?c=7697"/></ul></div>
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<div><pre>_____ _ _____ _____ _
|
||||
| |___ _____ ___ _ _| |_ ___ ___ ___ | _ |___ ___ | __ |___ _| |
|
||||
| --| . | | . | | | _| -_| _|_ -| | | _| -_| | __ -| .'| . |
|
||||
|_____|___|_|_|_| _|___|_| |___|_| |___| |__|__|_| |___| |_____|__,|___|
|
||||
a newsletter by |_| <a href="https://jbcrawford.us">j. b. crawford</a> <a href="/">home</a> <a href="http://eepurl.com/g3QF-L">subscribe</a> <a href="https://computer.rip/rss.xml">rss</a></pre><h1>
|
||||
>>> 2020-11-28 the verboten band
|
||||
</h1><p>
|
||||
To start: yes, long time no see. Well, COVID-19 has been like that. Some days I feel accomplished if I successfully check my email. I finally managed to clear out a backlog of an entire handfull of things that needed thoughtful responses, though, and so here I am, screaming into the void instead of at anyone in particular.
|
||||
</p><p>
|
||||
That said, let's talk a bit about radios. It is probably unsurprising by now that I have a long-running interest in radio and especially digital radio communications---but people who come to radio from all kinds of different perspectives run into one odd problem: the curious refusal of any receiver to tune to certain frequencies in the 800-900MHz range.
|
||||
</p><p>
|
||||
A lot of people have a general knowledge that this has to do with some kind of legal prohibition on reception of cellular phones. That's roughly correct, but to fully explain the matter requires going into some depth on two different topics: FCC regulation of radio devices, and the development of cellular phones. The first sounds more boring, so let's hit that one first.
|
||||
</p><p>
|
||||
Generally speaking, most electronic products manufactured or imported into the United States are subject to regulation by the Federal Communications Commission. Specifically, they generally require an "Equipment Authorization" from the FCC prior to being marketed. For purposes of this regulatory scheme, electronic devices can be broadly divided into two categories: intentional radiators and unintentional radiators.
|
||||
</p><p>
|
||||
An intentional radiator is something that is specifically intended to broadcast a radio signal, like, say, a cellular phone. Intentional radiators must be certified to comply with the specific Part of the FCC regulations relevant to the service for which they will be used. For example, cellular phones must be certified against Part 27, Wireless Communications Service, among others. The exact process varies by the part and can be involved, but it generally involves the manufacturer paying a certified test lab to perform certain tests and complying with various other filing requirements which include placing a label on the device which specifies its FCC approval. Device manufacturers must file with the FCC a description of how this label will appear before they receive approval to market the device, which is why the rough designs of unreleased devices are sometimes revealed by the rough drawings in these filings---tech journalists will watch these to get the dimensions of new iPhones, for example.
|
||||
</p><p>
|
||||
By the way, when I say the "FCC Regulations," if you want to follow along at home these are promulgated as 47 CFR. So Part 27, for example, refers to 47 CFR 27. The ever lovely Cornell LII has the whole thing for your entertainment: https://www.law.cornell.edu/cfr/text/47. There's some reading for when you need help falling asleep.
|
||||
</p><p>
|
||||
But that's all besides the point, I'm more interested in talking about unintentional radiators, devices which are not intended to produce RF radiation but may still do so as a result of the operation of the electronics---this is generally called a spurious emission, which is basically any RF emitted by accident. These devices are certified under Part 15 of the FCC regulations[1], and so are sometimes called "Part 15 devices." Part 15 essentially limits the type and amplitude of spurious emissions to prevent random devices causing harmful interference due to defects in their designs.
|
||||
</p><p>
|
||||
What would we call a radio <em>receiver</em>, then? It is explicitly a radio device, but is not intended to transmit anything. As a result, radio receivers are Part 15 devices. Most of Part 15 is very general and doesn't really say anything specific about radio devices, it just limits spurious emissions and other design standards. However, 15.121 gets a great deal more specific in discussing "Scanning receivers.' A scanning receiver is specifically defined earlier in the regulation as a device capable of tuning to two or more frequency bands in the range of 30-960Mhz. This has the fun result that nothing for the GHz range is technically a scanner, but for practical reasons this doesn't matter too much.
|
||||
</p><p>
|
||||
So what's in 15.121? This is:
|
||||
</p><p>
|
||||
47 CFR 15.121(a): ... scanning receivers and frequency converters designed or marketed for use with scanning receivers, shall: (1) Be incapable of operating (tuning), or readily being altered by the user to operate, within the frequency bands allocated to the Cellular Radiotelephone Service in part 22 of this chapter (cellular telephone bands). ... (b) Be designed so that the tuning, control and filtering circuitry is inaccessible. The design must be such that any attempts to modify the equipment to receive transmissions from the Cellular Radiotelephone Service likely will render the receiver inoperable.
|
||||
</p><p>
|
||||
The rest of paragraph (a) gives a pretty long clarification of "readily being altered by the user," and it's amusing to think of a bunch of FCC characters sitting around a table trying to think up every alteration that is easy. Jumper wires and reprogramming micro-controllers are both right out.
|
||||
</p><p>
|
||||
It gets even better:
|
||||
</p><p>
|
||||
47 CFR 15.121(b): ... scanning receivers shall reject any signals from the Cellular Radiotelephone Service frequency bands that are 38 dB or lower based upon a 12 dB SINAD measurement, which is considered the threshold where a signal can be clearly discerned from any interference that may be present.
|
||||
</p><p>
|
||||
So, here's this actual weird rule about scanners. Scanners are specifically prohibited from being able to tune to any bands allocated to the Part 22 Cellular Radiotelephone Service. This raises questions, and as you can imagine from the way I got here, I am about to spend a long time answering them.
|
||||
</p><p>
|
||||
When the FCC says "Cellular Radiotelephone Service," they aren't talking about cell phones in general. The CRS as I'll call it refers to a <em>very specific</em> cellular service, and that is AMPS.
|
||||
</p><p>
|
||||
AMPS, the Advanced Mobile Phone System, is the most common in the US of the "1G" cellular services. Most carriers that were around when it was offered called it "Analog" service, and indeed, AMPS was entirely analog. And, due to an odd detail of the regulation, large cellular carriers were <em>required</em> to offer AMPS service until 2008, long after AMPS phones were no longer produced. You may have had a candy bar phone back when you would occasionally see an "A" for analog service, but I hope not into the late 2000s.
|
||||
</p><p>
|
||||
There are a few things that we might infer from AMPS being an analog service. One of those things is that it probably did not employ strong encryption. In fact, AMPS employed no scrambling or enciphering of any kind. Your phone conversations were just flapping in the wind for anyone to hear. This posed a major practical problem for carriers in the '90s as it was discovered that it was not particularly difficult to intercept the call setup process from an AMPS phone and swipe its identification numbers, allowing you to basically steal someone else's cellular service. You can imagine that this was popular with certain criminals with a need for untraceable but convenient communications.
|
||||
</p><p>
|
||||
There was also a problem for consumers: their phone conversations could be fairly easily overheard. There were a number of ways to do this, using any radio scanner that covered that band for example. One particularly well-known option was a particular model of phone, the Oki 900, that had an unusually open design (in terms of modifiability) that led to reverse engineered and modified firmware being developed that made eavesdropping on other people's calls just, well, a feature it had.
|
||||
</p><p>
|
||||
The scale of this problem was fairly large, and it was fairly well known. For example, let's turn to <em>my</em> favorite source of late-night reading, newspaper archives. A lovely piece in the 30 May 1990 issue of The News and Observer, from Raleigh NC, takes the cheesy headline "Monitoring Megahertz" and goes into some depth on the issue.
|
||||
</p><p>
|
||||
"I've heard men call their wives and tell them they'll be home late, then call their girl friends," quipped one electronics store owner who had "accidentally" eavesdropped on cellular calls using a scanner. We've all fat-fingered our ways into someone else's affairs I'm sure, pun intended. Another person said "when you look at the fact that there are how many thousands of people out there who know my name, my mailing address and my salary...I put cellular eavesdropping down as being no different from that." In the face of technology, even in 1990, people had begun to abandon their privacy.
|
||||
</p><p>
|
||||
Cellular carriers were not so happy about this, viewing it as an embarrassment to their operation. I have heard before that cellular carriers went so far as to lobby for banning scanners entirely, although I am not aware of much hard evidence of this. What they did do was convince congress to stick an extra few paragraphs onto an otherwise only tangentially related bit of legislation called the Telephone Disclosure and Dispute Resolution Act of 1992. This has largely to do with abusive 1-900 numbers, which is its whole own topic in telephone regulation that I ought to take on sometime. But it also brought along just a bit more, an extra section that was subsequently amended several times at the behest of cellular carriers. Let's read part of it, as amended, and with some editing for readability.
|
||||
</p><p>
|
||||
The Commission shall prescribe and make effective regulations denying equipment authorization for any scanning receiver that is capable of---(A) receiving transmissions in the frequencies allocated to the domestic cellular radio telecommunications service, (B) readily being altered by the user to receive transmissions in such frequencies, or (C) being equipped with decoders that convert digital cellular transmissions to analog voice audio.
|
||||
</p><p>
|
||||
Well, we've made it full circle: we've seen the regulation, and we've seen the legislation that kicked the FCC to write the regulation. But how does this translate today? Things get a bit weird there.
|
||||
</p><p>
|
||||
You see, the FCC seems to have (sensibly) interpreted the legislation as applying directly to the Cellular Radiotelephone Service, even though the legislation actually uses the term "domestic cellular radio communications service" which seems almost equally lively to have been (1) intended to be more general in its applicability or (2) a result of someone drafting legislation having read "Cellular Radiotelephone Service" in the FCC regulations but then forgetting exactly how it was worded.
|
||||
</p><p>
|
||||
The Cellular Radiotelephone Service was allocated 824-849MHz and later 869-894MHz. That's it. You see, all of the digital cellular systems we use today are considered completely different services from Cellular Radiotelephone (usually called Wireless Communications Service although the details get complex). As a result, and to this day, those two sections in the 800MHz band are verboten to scanners, and nothing else.
|
||||
</p><p>
|
||||
And about those frequencies... after the requirement for AMPS service ended, all US carriers ceased AMPS operations. The old AMPS bands remain allocated for cellular service, and Verizon and a couple of smaller carriers use the same frequencies for digital cellular services, which employ encryption and cannot be intercepted by radio scanners. The prohibition on tuning scanners to these frequencies no longer makes any sense, especially since this ban has never been extended to the AWS, PCS, and WCS bands that are more widely used by modern cellular phones.
|
||||
</p><p>
|
||||
My suspicion is that the fact that this regulation was mandated by congress makes it difficult for the FCC to remove or modify, even though it no longer makes technical sense. Unless congress finds some time for minutiae we are unlikely to see a change in this rule.
|
||||
</p><p>
|
||||
In general, the whole thing is sort of bizarre. Broadly speaking, it is legal to listen in on any radio communications in the US, but cellular phones have repeatedly gotten a special carve-out.
|
||||
</p><p>
|
||||
Repeatedly? That's right. The whole AMPS band and scanners rule is the only specific <em>technical</em> regulation, but the Electronic Communications Privacy Act of 1986 had actually already made it illegal to intercept or listen in on cellular calls, and this remains true to the present day... but there was virtually no enforcement, and that hasn't really changed to this day.
|
||||
</p><p>
|
||||
And of course the whole thing has always felt like a farce. The solution to the poor (or rather nonexistent) security design of AMPS was never legislation, but cellular carriers and the congress will be damned if they didn't try. In practice, the rule swept the entire eavesdropping problem under the rug for some years, allowing carriers to continue operating the insecure AMPS system for far longer than they should have (...but exactly as long as the FCC required them to).
|
||||
</p><p>
|
||||
Because listening to the modern digital cellular modes wouldn't be particularly interesting or useful anyway, and this rule doesn't really deter anyone with the motivation and ability to decode those modes anyway, there are two lasting impacts of this rather particular rule:
|
||||
</p><p>
|
||||
1) SDRs and other receivers made today must implement this particular and peculiar restriction in order to receive US equipment authorization, which is probably part of the reason that a lot of SDRs... don't.
|
||||
</p><p>
|
||||
2) To comply with the specifics of the regulation about rejection, many receivers use a notch filter around 850MHz in their frontend. This means that reception throughout the 800-900MHz range is particularly poor, a real irritation as various public agencies and private agencies (especially railroads) use land-mobile radios elsewhere in the 800-900Mhz range.
|
||||
</p><p>
|
||||
Basically, more than a decade after any of this made sense, we're all still hassling with it.
|
||||
</p><p>
|
||||
[1] Part 15 is actually a lot more general and unintentional radiators are specifically discussed under 47 CFR 15.101, but everyone just says Part 15.
|
||||
</p><pre>sincerely,
|
||||
j. b. crawford
|
||||
me@computer.rip
|
||||
|
||||
|
||||
This website is begrudgingly generated by the use of software. Letters to the
|
||||
editor are welcome via facsimile to +1 (505) 926-5492 or mail to 609 Gold Ave
|
||||
SW, Suite 1D, Albuquerque, NM 87102. Opinions stated here are somewhat
|
||||
necessarily those of <a href="https://7thstd.com">Seventh Standard, LLC</a>, in that the author is the sole
|
||||
partner and does not wish to lead dual lives.</pre></div>
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
<div><h1>
|
||||
<a href="https://www.computerenhance.com">Computer, Enhance!</a>
|
||||
</h1><figure><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F43e258db-6164-4e47-835f-d11f10847d9d_5616x3744.jpeg 424w, https://substackcdn.com/image/fetch/w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F43e258db-6164-4e47-835f-d11f10847d9d_5616x3744.jpeg 848w, https://substackcdn.com/image/fetch/w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F43e258db-6164-4e47-835f-d11f10847d9d_5616x3744.jpeg 1272w, https://substackcdn.com/image/fetch/w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F43e258db-6164-4e47-835f-d11f10847d9d_5616x3744.jpeg 1456w" sizes="100vw"/><img src="https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F43e258db-6164-4e47-835f-d11f10847d9d_5616x3744.jpeg" alt="A sculpture of a cartoon character stuck in a pipe." title="A sculpture of a cartoon character stuck in a pipe." srcset="https://substackcdn.com/image/fetch/w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F43e258db-6164-4e47-835f-d11f10847d9d_5616x3744.jpeg 424w, https://substackcdn.com/image/fetch/w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F43e258db-6164-4e47-835f-d11f10847d9d_5616x3744.jpeg 848w, https://substackcdn.com/image/fetch/w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F43e258db-6164-4e47-835f-d11f10847d9d_5616x3744.jpeg 1272w, https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F43e258db-6164-4e47-835f-d11f10847d9d_5616x3744.jpeg 1456w" sizes="100vw"/></picture></figure><p>
|
||||
<span>By now I</span> <a href="https://twitter.com/cmuratori/status/1543874684868931584" rel="">should know better</a> <span>than to ask on Twitter for a “rigorous analysis” of anything. As George W. Bush said, “Fool me once, shame on you…</span> <a href="https://www.youtube.com/watch?v=ntwdH3Q54ZY" rel="">fool me can’t fooled again</a><span>.”</span>
|
||||
</p><p>
|
||||
I don’t want to be “fool me can’t get fooled again”, so I officially give up on technical tweets. Today’s the last day I will ever post anything technical on Twitter, I promise. Instead, you will be forced to endure yet another Substack, so I can post 3,000-word posts that no one will read.
|
||||
</p><p>
|
||||
Here we go:
|
||||
</p><p>
|
||||
The goal with raw UDP is very simple: better performance and security on the server side.
|
||||
</p><p>
|
||||
<span>HTTPS is an unbaked sausage made by grinding pure text HTTP with TLS and encasing the result in an arbitrary selection of third-party animal intestine… err, I mean, “highly secure” certificates provided by arbitrarily selected certificate providers. Implementing HTTPS is a massive amount of code that is inexorably slow. It is not only theoretically difficult to secure completely, but is</span> <a href="https://www.openssl.org/news/vulnerabilities.html" rel="">insecure in practice</a> <span>in popular implementations available to the public.</span>
|
||||
</p><p>
|
||||
<span>Oh, and the certificate authorities are also insecure, by the way - but that’s</span> <a href="https://en.wikipedia.org/wiki/DigiNotar" rel="">another story</a> <span>(and</span> <a href="https://decoded.avast.io/luigicamastra/backdoored-client-from-mongolian-ca-monpass/" rel="">another</a><span>, and</span> <a href="https://www.computerworld.com/article/2507090/firm-points-finger-at-iran-for-ssl-certificate-theft.html" rel="">another</a><span>, and</span> <a href="https://sslmate.com/resources/certificate_authority_failures" rel="">…</a><span>)</span>
|
||||
</p><p>
|
||||
It also relied (up until recently) on TCP, which, unless you plan to write a completely custom network stack for every type of server/NIC you ever use, requires the underlying kernel to understand and track network connections. This means that you inherit substantial overhead, and perhaps vulnerabilities as well, from the TCP/IP substrate before you even begin to write your server code.
|
||||
</p><p>
|
||||
If you were a large company with significant academic and engineering resources, you might instead want to design your own private secure protocol that:
|
||||
</p><ol><li><p>
|
||||
Uses encryption you control, so it cannot be bypassed by hacking the certificate authority,
|
||||
</p></li><li><p>
|
||||
Uses UDP to avoid having OS connection state on the server side, and
|
||||
</p></li><li><p>
|
||||
Uses a well-designed, known packet structure to improve throughput and reduce security vulnerabilities from HTTP/TLS parsing.
|
||||
</p></li></ol><p>
|
||||
<span>The first thing on that list is half-possible now. Although there’s nothing you can (ever</span><a href="#footnote-1" rel="">1</a><span>) do to avoid man-in-the-middle attacks the very first time someone interacts with your server, web APIs have long made it possible to store data on the client for later use. One use for that data would be storing your own set of public keys.</span>
|
||||
</p><p>
|
||||
<span>So even using nothing newer than XHR and cookies, you could theoretically add your own layer of encryption to anything you send to the server. This would ensure that any subsequent hack of the certificate authority could not inspect or modify your packets. It’d be much less efficient than rolling your own top-to-bottom, because now you pay the entire cost for your encryption</span> <em>and</em> <span>TLS. But you</span> <em>can</em> <span>do it.</span>
|
||||
</p><p>
|
||||
<span>It’s slow, but possible. Call it</span> <em>half-possible</em><span>, like I did above.</span>
|
||||
</p><p>
|
||||
<span>The second thing on the list is sort-of possible now as well. If you can somehow manage to use</span> <a href="https://en.wikipedia.org/wiki/HTTP/3" rel="">HTTP/3</a> <span>exclusively as your target platform, you will still be talking HTTP but you’ll be doing it over UDP instead of TCP, and can manage connection state however you wish without OS intervention.</span>
|
||||
</p><p>
|
||||
<span>It is probably unrealistic to assume that you could do this in practice today. If you didn’t care about broad compatibility, you probably wouldn’t be deploying on the web anyway, so presumably the current adoption of HTTP/3 is insufficient. But at least it</span> <em>exists</em><span>, and perhaps if adoption continues to grow,</span> <em>eventually</em> <span>it will be possible to require HTTP/3 without losing a significant number of users. For now, it’s only something you can do on the side - you still have to have a traditional HTTPS fallback.</span>
|
||||
</p><p>
|
||||
<span>Which brings us to the third item on the list, and the real sticking point. As far as I’m aware, no current or planned future Web API ever lets you do number three. There are many new web “technologies” swarming around the custom packet idea (</span><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API" rel="">WebRTC</a><span>,</span> <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API" rel="">WebSockets</a><span>,</span> <a href="https://github.com/w3c/webtransport" rel="">WebTransport</a><span>), but to the best of my knowledge, all of them require an HTTPS connection to be made first, so your “custom packet” servers still need to implement all of HTTPS anyway.</span>
|
||||
</p><p>
|
||||
I can imagine someone raising the following objection at this point: “If you don’t support HTTPS on the server, how do you serve the WASM/JavaScript/whatever with the custom packet logic in the first place?”
|
||||
</p><p>
|
||||
That’s a reasonable question.
|
||||
</p><p>
|
||||
The answer is, the two most logical deployment scenarios I can think of both involve a separate server (or process) for the initial HTTPS transaction.
|
||||
</p><p>
|
||||
The first is what I imagine would be the most common: you upload to a CDN a traditional web package containing the PWA-style web worker necessary to do your own custom packet logic. The CDN serves this (static) content everywhere for you. They obviously implement HTTPS already, because that’s what they do for a living, and they’re not your servers anyway so you don’t care.
|
||||
</p><p>
|
||||
<span>The second would be less common, but plausible: you run your own CDN-equivalent, because</span> <a href="https://knowyourmeme.com/memes/chuck-norris-facts" rel="">you’re just that hard core</a><span>. But you expect that your HTTPS code is more vulnerable than your custom code, since HTTPS is vastly more complicated and has ridiculous things in it like arbitrary text parsing, which no one in their right mind would ever put into a “secure” protocol. So you cabin your HTTPS server instances into their own restricted processes or own machines entirely. This prevents exploits of the HTTPS code from affecting anything other than newly connecting users - existing users (who are only talking to your custom servers) remain unharmed.</span>
|
||||
</p><p>
|
||||
In neither scenario do you actually include HTTPS code in any of the processes running your actual secure server.
|
||||
</p><p>
|
||||
So that’s the hopefully-at-least-somewhat-convincing explanation of why someone might want raw UDP. Now the question is, can raw UDP be provided by a browser in a way that is “secure”?
|
||||
</p><p>
|
||||
<span>I’m putting a lot of these words in scare quotes because browsers</span> <em>aren’t</em> <span>secure for any serious definition of that word, and hopefully that is overwhelmingly obvious to everyone who has ever used one. But just to be clear about the landscape, there are two different ways browsers are not secure:</span>
|
||||
</p><ol><li><p>
|
||||
<span>The web as a platform consists of massive, overlapping, poorly-specified APIs that require millions of lines of code to fully implement. As a result, browsers inexorably have</span> <a href="https://www.mozilla.org/en-US/security/known-vulnerabilities/firefox/" rel="">an effectively infinite number of security exploits</a> <span>waiting to be found.</span>
|
||||
</p></li><li><p>
|
||||
Browsers include the ability, sans exploit, to transmit information from the client computer to any number of remote servers. Without the ability to control this behavior, the user’s data could be misappropriated.
|
||||
</p></li></ol><p>
|
||||
Clearly, for raw UDP, we only care about the second one of these. The first one happens in browsers all the time already and there’s no reason to suspect that raw UDP would somehow have more implementation code vulnerabilities on average than any other part of the sprawling browser substrate.
|
||||
</p><p>
|
||||
<span>So the question is, assuming the browser</span> <em>has not</em> <span>been exploited, what is the security standard for web features, and can raw UDP be implemented under that standard or not?</span>
|
||||
</p><p>
|
||||
As a point of comparison, I will use the example of the current camera/microphone/location policy as it presently exists. That will be our “gold standard”, since if it were not considered “secure” by web implementers, presumably it would not have been knowingly shipped in web browsers everywhere for the past several years.
|
||||
</p><p>
|
||||
As everyone who uses a web browser knows, a web site at present is allowed to ask you for permission, temporarily or permanently (your choice), to access your camera, microphone, and location data. Once you say “yes” to any one of these things, that site can transmit that data anywhere in the world, and use it for any purpose, trivially.
|
||||
</p><p>
|
||||
Allow me to provide a worked example.
|
||||
</p><p>
|
||||
<span>Suppose I partner with Jeffrey Toobin to make a cybersex conduit site for people who, like him, see the value in quickly switching tabs away from your work meetings to get down to some</span> <em>real</em> <span>business. We launch cyberballsdeep.net, and it’s a big success.</span>
|
||||
</p><p>
|
||||
When a user visits our site, they see at most two security-related things:
|
||||
</p><ol><li><p>
|
||||
An allow/deny request for access to the microphone and camera, and
|
||||
</p></li><li><p>
|
||||
A lock icon indicating that the connection has been signed by a third party warranting that this connection is end-to-end encrypted from the user’s machine to some server somewhere with the secure keys for cyberballsdeep.net.
|
||||
</p></li></ol><p>
|
||||
Assuming you click “allow” - which you have to in order to use the service - the servers at cyberballsdeep.net can now do anything they want with your (very sensitive) video data. They can, for example, record you while you are toobin’ and play it back at any time, anywhere, at their discretion. They could play it on a billboard in Times Square, they could send it to your spouse - anything goes.
|
||||
</p><p>
|
||||
So the “security standard” that you are getting, in practice, exactly mirrors the two things you saw:
|
||||
</p><ol><li><p>
|
||||
You know your sensitive data will not be captured unless you click “allow”, and
|
||||
</p></li><li><p>
|
||||
You know that nobody will be able to see your sensitive data unless either cyberballsdeep.net or the issuing certificate authority let them (either intentionally, or unintentionally if they’ve been hacked).
|
||||
</p></li></ol><p>
|
||||
<span>That’s it. You don’t know anything else. In practice, you basically have no security guarantees other than a warrant that your sensitive data will go to a particular named party</span> <em>first</em> <span>before it goes somewhere else.</span>
|
||||
</p><p>
|
||||
<span>Hopefully we can all agree that this extremely low bar for security is the only hurdle one should have to clear in order to dismiss concerns of “security” as a reason not to implement a feature in a W3C spec. It’s not much, but it is</span> <em>something</em><span>.</span>
|
||||
</p><p>
|
||||
<span>OK, finally, with all that out of the way, this is what I actually wanted someone to point me to</span> <a href="https://twitter.com/cmuratori/status/1543874684868931584" rel="">when I asked about this on Twitter</a><span>. I just wanted to see that someone, somewhere, had worked out exactly why UDP could not be made to fit the same security model considered acceptable across other basic web features already deployed and considered “secure”.</span>
|
||||
</p><p>
|
||||
Since nobody sent me such a thing, I am still stuck with my own security modeling, with nothing to compare against. My model goes something like this:
|
||||
</p><p>
|
||||
Step one - the “allow/deny” step - is easy for raw UDP to provide. The browser is still sitting between the JavaScript/WASM layer and the OS sockets layer, so it can ensure that inbound and outbound packets are filtered any way the browser wishes.
|
||||
</p><p>
|
||||
This means that it would be trivial for a browser to only allow UDP packets to and from servers that the user has authorized, as it does with microphone, camera, and location data. Any site that wishes to access raw UDP simply provides a hostname to the browser, and the browser asks the user whether they wish to allow the page to communicate with that site.
|
||||
</p><p>
|
||||
Furthermore, since the browser already allows the page to send as much HTTPS data as it wants back to the originating site, one could optionally allow any site to send UDP packets back to its own (exact) originating IP without asking the user. This is not necessary for raw UDP to work, but I can’t think of any violation of “step one” that would happen as a result, so it could be considered.
|
||||
</p><p>
|
||||
<span>Note that this is</span> <em>not</em> <span>true for something like camera/microphone/location data. Those are additional data sources to which the page gets access, so if anything, raw UDP permission is</span> <em>less</em> <span>dangerous in terms of user permission, since at no time does the page itself get additional access to the user’s data, regardless of whether they allow UDP communication.</span>
|
||||
</p><p>
|
||||
Which brings us to step two.
|
||||
</p><p>
|
||||
As far as I can tell, there’s actually nothing special about step two. The original web page was served by HTTPS, obviously, since that’s the only way the browser supports getting WASM/JavaScript downloaded in the first place. So the originating server and code are already exactly as “secure” as they would be in any other scenario.
|
||||
</p><p>
|
||||
The user had to affirmatively allow the destination name, so the page can only send UDP to a specifically approved endpoint.
|
||||
</p><p>
|
||||
<span>So the only question is,</span> <em>can the user be sure that the data sent to that endpoint is encrypted such that only the endpoint or the certificate authority can decrypt it?</em>
|
||||
</p><p>
|
||||
<span>I can’t know the hivemind of a W3C committee (thank the heavens). But if I had to guess, I would suspect that this is why they didn’t want to allow raw UDP (or raw TCP for that matter). In their mind, it probably seems</span> <em>less secure</em> <span>than HTTPS to allow a web page to implement its own secure UDP protocol.</span>
|
||||
</p><p>
|
||||
<span>However, to my mind, this is based upon a flawed assumption. That assumption is that somehow web implementers</span> <em>can</em> <span>be trusted to deploy their encryption keys securely, but</span> <em>cannot</em> <span>be trusted to deploy their protocol securely.</span>
|
||||
</p><p>
|
||||
To be more specific, HTTPS can be intercepted trivially if the attacker A) has a machine on the route between the endpoints and B) has access to the server’s keys, or any certificate authority’s signing capability. (A) either happens or it doesn’t - there’s no way to control it - so (B) is really the entire question.
|
||||
</p><p>
|
||||
So the notion that allowing web pages to use UDP for transmission is less secure than HTTPS seems to me to be predicated on the notion that web developers can be trusted to do something complicated in one place (run a set of servers without leaking keys), but also cannot be trusted to do something complicated in another (download, for example, a JavaScript UDP encryption library and use it).
|
||||
</p><p>
|
||||
Stated alternately, the hard constraint on the client side that you can’t roll your packet code “for security reasons” is nowhere to be found on the server side. There is no requirement anywhere in W3C or anywhere else that says your web server has to be… well… anything at all, really. You can just go ahead and write your own code from top to bottom. You can even have a dedicated web page on your site that has the entire cryptographic key set for the server posted on it for people to cut-and-paste, so everyone can impersonate your server to anyone, anywhere, at any time. You can leave a thumb drive with your keys at the bar. You can generate your keys with a random seed of 0x000000000000000000. Anything goes.
|
||||
</p><p>
|
||||
<span>Nobody seems to be panicked about this. Nobody has pushed the policy that the W3C should standardize on a specific web server deployment that you are forced to use, or a set of n of them made by Google/Mozilla/Apple, or what have you. It is just assumed that everyone is allowed to write their own</span> <em>server</em> <span>packet handling, but that no one is allowed to write their own</span> <em>client</em> <span>packet handling.</span>
|
||||
</p><p>
|
||||
So that’s what I would like explained. Internet, justify this!
|
||||
</p><p>
|
||||
I have seen people mention (but not support) a claim that raw UDP would cause “denial of service” problems because malicious web pages would send UDP packets to random servers in an attempt to overload them. This claim seems completely baseless to me, because there is no reason why you can’t employ the relevant XHR DDoS restrictions to UDP. If DDoS was the concern, just require that UDP packets be sent exclusively within the same domain as the originating code.
|
||||
</p><p>
|
||||
<span>Furthermore, you could restrict the port ranges of raw web UDP to some assigned range. A new port range could be explicitly reserved</span> <em>just for raw web UDP</em> <span>if that makes people more comfortable, so it could literally be discarded at the gateway on any network that doesn’t want to support raw UDP for web, making it easier to deal with than UDP attacks from native code and viruses which can choose their ports at will.</span>
|
||||
</p><p>
|
||||
<span>At that point, I fail to see how raw UDP from the browser could be significantly more dangerous than XHR, unless I am missing some particularly clever use of UDP. And again,</span> <em>that’s why I asked for writeups in my original tweet</em><span>. I’m totally willing to believe I’m missing something, but I want to see a complete technical explanation about what it is.</span>
|
||||
</p><p>
|
||||
<span>Now, none of this is the same as saying I can’t see how you would perform DDoS attacks with raw UDP. I certainly can. I just can’t see how you would perform them</span> <em>more easily than with XHR,</em> <span>which obviously is considered “secure”</span><em>.</em>
|
||||
</p><p>
|
||||
As a simple example, suppose a commercial CDN distributes the payload of ddosfuntimes.com. On the main page, there’s an XHR to target.ddosfuntimes.com. Even though the CDN is a completely different set of IP addresses as target.ddosfuntimes.com, this is completely legal under XHR policy.
|
||||
</p><p>
|
||||
The owners of ddosfuntimes.com can go ahead and set the IP address in their DNS records to point target.ddosfuntimes.com at any server they want, and they will receive all the XHR traffic from every browser that visits the page. And to the best of my knowledge, there isn’t a damn thing the target can do about that.
|
||||
</p><p>
|
||||
So unless I’m missing something, XHR already allows you to target any website you wish with unwanted traffic from anyone who visits your site. So why the concern about UDP?
|
||||
</p></div>
|
||||
107
packages/readabilityjs/test/test-pages/dailymail/distiller.html
Normal file
107
packages/readabilityjs/test/test-pages/dailymail/distiller.html
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<div><h2>
|
||||
EXCLUSIVE: Welcome to the neighborhood! Pregnant Katharine McPhee and David Foster are house hunting in Harry and Meghan's star-studded Montecito enclave to be closer to their royal BFFs and have play dates with Archie
|
||||
</h2><ul><li><strong>David Foster and his pregnant wife Katharine McPhee are house hunting in Montecito so they can live near Harry and Meghan, DailyMail.com can reveal</strong></li><li><strong>A source close to Foster, 70, and McPhee, 36, said they want their child to grow up alongside Archie in the family friendly area near Santa Barbara, California </strong></li><li><strong>'Now that Katharine is pregnant they both want to settle down and raise their child in a nice area with nice schools,' our source said </strong></li><li><strong>Meghan and McPhee are longtime friends, having gone to school together and performed in musicals in Southern California </strong></li><li><strong>Since the Sussexes moved to Montecito, music producer Foster and McPhee have spent a lot of time there and fallen in love with the area </strong></li><li><strong>Meghan and Harry bought a nine-bedroom and 16-bathroom mansion in the secluded area of Riven Rock in upscale Montecito in June for $14.65m</strong></li><li><strong>Katy Perry and Orlando Bloom have just snapped up a gorgeous Mediterranean villa in Montecito, California for $14.2 million </strong></li></ul><ul><img src="https://i.dailymail.co.uk/i/furniture/articles/spt_socialicons_40x40eachbttn9.png"/><img src="https://i.dailymail.co.uk/i/furniture/articles/spt_socialicons_40x40eachbttn9.png"/><img src="https://i.dailymail.co.uk/i/furniture/articles/spt_socialicons_40x40eachbttn9.png"/></ul><p>
|
||||
<a href="/news/prince_harry/index.html">Prince Harry</a> and Meghan's close friends David Foster and Katharine McPhee are house hunting in Montecito so they can live near the royal couple, DailyMail.com can reveal.
|
||||
</p><p>
|
||||
It's understood music producer Foster and his pregnant singer/actress wife McPhee have been looking at properties in the exclusive celebrity enclave and are considering making a permanent move.
|
||||
</p><p>
|
||||
A source close to Foster, 70, and McPhee, 36, said they want their child to grow up alongside Archie in the family friendly area near Santa Barbara.
|
||||
</p><p>
|
||||
The source told DailyMail.com: 'David and Katharine have become really close to Harry and Meghan.
|
||||
</p><p>
|
||||
'And now Katharine is pregnant they both want to settle down and raise their child in a nice area with nice schools.
|
||||
</p><p>
|
||||
'They've spent quite a bit of time in Montecito since Harry and Meghan moved there and they've fallen in love with it so they decided to start looking at properties to see what's on the market.
|
||||
</p><p>
|
||||
'If they do make the move their child can play with Archie and become good friends. Harry and Meghan are excited about having them live nearby and they all want their kids to grow up together.'
|
||||
</p><img src="https://i.dailymail.co.uk/1s/2020/10/22/20/34711246-8868177-image-m-39_1603395562081.jpg" alt="David Foster and his pregnant wife Katharine McPhee are house-hunting in Montecito so they can live near Harry and Meghan, DailyMail.com can reveal"/><img src="https://i.dailymail.co.uk/i/furniture/mobile/icon_camera_90x68.png"/><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
David Foster and his pregnant wife Katharine McPhee are house-hunting in Montecito so they can live near Harry and Meghan, DailyMail.com can reveal
|
||||
</p><img src="https://i.dailymail.co.uk/1s/2020/10/22/20/34711200-8868177-A_source_close_to_Foster_70_and_McPhee_36_said_they_want_their_c-m-42_1603395858492.jpg" alt="A source close to Foster, 70, and McPhee, 36, said they want their child to grow up alongside Archie in the family friendly area near Santa Barbara, California"/><img src="https://i.dailymail.co.uk/i/furniture/mobile/icon_camera_90x68.png"/><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
A source close to Foster, 70, and McPhee, 36, said they want their child to grow up alongside Archie in the family friendly area near Santa Barbara, California
|
||||
</p><img src="https://i.dailymail.co.uk/1s/2020/10/22/19/34170100-8868177-The_couple_bought_the_mansion_known_as_The_Chateau_for_14_65_mil-a-16_1603392604855.jpg" alt="Meghan and Harry bought the mansion, known as 'The Chateau', for $14.65 million on June 18, making them neighbors with celebrities Oprah Winfrey and Ellen DeGeneres"/><img src="https://i.dailymail.co.uk/i/furniture/mobile/icon_camera_90x68.png"/><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
Meghan and Harry bought the mansion, known as 'The Chateau', for $14.65 million on June 18, making them neighbors with celebrities Oprah Winfrey and Ellen DeGeneres
|
||||
</p><p>
|
||||
Foster and McPhee currently live in the music producer and Real Housewives of Beverly Hills alum's LA apartment, but it's not clear whether he will be selling up.
|
||||
</p><p>
|
||||
The two couples have become firm friends since Harry and Meghan moved to California.
|
||||
</p><p>
|
||||
And they enjoyed a cozy dinner date together in Montecito last week to celebrate McPhee's pregnancy.
|
||||
</p><p>
|
||||
The outing was the first glimpse of the Duke, 36, and Duchess of Sussex, 39, in Montecito since DailyMail.com exclusively revealed they had moved into a sprawling $14 million mansion in the leafy Californian town.
|
||||
</p><p>
|
||||
The couple bought the mansion, known as 'The Chateau', for $14.65 million on June 18, making them neighbors with celebrities Oprah Winfrey and Ellen DeGeneres.
|
||||
</p><p>
|
||||
The 14,563-square-foot home, known as 'The Chateau', sits on 5.4 acres of land and immaculately clipped hedges border the estate's stone-pillared entry gates.
|
||||
</p><p>
|
||||
Foster, a 16-time Grammy winning composer and music producer, and McPhee have a few properties to choose from in the area.<span><span> </span></span>
|
||||
</p><img src="https://i.dailymail.co.uk/1s/2020/10/22/19/34710656-8868177-image-a-13_1603392416393.jpg" alt="Depending on their budget they could outdo the Sussexes by stumping for this stunning $16.9million, 12.000 sq ft Italian style estate called Villa Bencistà"/><img src="https://i.dailymail.co.uk/i/furniture/mobile/icon_camera_90x68.png"/><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
Depending on their budget they could outdo the Sussexes by stumping for this stunning $16.9million, 12.000 sq ft Italian style estate called Villa Bencistà
|
||||
</p><img src="https://i.dailymail.co.uk/1s/2020/10/22/19/34710478-8868177-image-a-30_1603392790435.jpg" alt="If that's too pricey then a smaller, but more modern $6.4million architectural estate is also on offer, boasting a swimming pool and 4,000 sq ft of space"/><img src="https://i.dailymail.co.uk/i/furniture/mobile/icon_camera_90x68.png"/><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
If that's too pricey then a smaller, but more modern $6.4million architectural estate is also on offer, boasting a swimming pool and 4,000 sq ft of space
|
||||
</p><img src="https://i.dailymail.co.uk/1s/2020/10/22/19/34710930-8868177-image-a-26_1603392706891.jpg" alt="If Katharine and David are looking for something a little more cozy, then there's a beautiful single-level, town home also on the market, a snip at $2.3million"/><img src="https://i.dailymail.co.uk/i/furniture/mobile/icon_camera_90x68.png"/><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
If Katharine and David are looking for something a little more cozy, then there's a beautiful single-level, town home also on the market, a snip at $2.3million
|
||||
</p><p>
|
||||
Depending on their budget they could outdo the Sussexes by stumping for a stunning $16.9million, 12.000 sq ft Italian style estate called Villa Bencistà.
|
||||
</p><img src="https://i.dailymail.co.uk/1s/2020/10/22/20/34642374-8868177-Katy_Perry_and_Orlando_Bloom_have_just_snapped_up_a_gorgeous_Med-m-40_1603395757376.jpg" alt="Katy Perry and Orlando Bloom have just snapped up a gorgeous Mediterranean villa in the wealthy enclave of Montecito, California for $14.2 million"/><img src="https://i.dailymail.co.uk/i/furniture/mobile/icon_camera_90x68.png"/><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
Katy Perry and Orlando Bloom have just snapped up a gorgeous Mediterranean villa in the wealthy enclave of Montecito, California for $14.2 million
|
||||
</p><p>
|
||||
The sprawling estate, owned by Monica Allyn, the wife of film producer Rob Allyn, came on the market earlier this month.
|
||||
</p><p>
|
||||
If that's too pricey then a smaller, but more modern $6.4million architectural estate is also on offer, boasting a swimming pool and 4,000 sq ft of space.
|
||||
</p><p>
|
||||
But if they're looking for something a little more cozy, then there's a beautiful single-level, town home also on the market, a snip at $2.3million.
|
||||
</p><p>
|
||||
Harry and Meghan have bonded with Foster and McPhee since leaving the royal family and arriving in California.
|
||||
</p><p>
|
||||
Also new to the neighborhood are <a href="/tvshowbiz/katy_perry/index.html">Katy Perry</a> and Orlando Bloom who snapped up an exquisite compound in Montecito for $14.2 million.
|
||||
</p><p>
|
||||
The couple, who welcomed daughter Daisy Dove in August, purchased the stunning ocean view home from former Duracell and Chrysler CEO C. Robert Kidder, who owned the estate for over 20 years, according to <a rel="nofollow noreferrer noopener" target="_blank" href="https://variety.com/2020/dirt/entertainers/katy-perry-orlando-bloom-buy-14-2-million-montecito-compound-1234811052/#">Variety</a>.
|
||||
</p><p>
|
||||
The couple's gated property consists of multiple structures sitting upon 8.9 acres of land and will be situated among Hollywood's elite. <span> </span>
|
||||
</p><img src="https://i.dailymail.co.uk/1s/2020/10/22/20/34711210-8868177-image-m-32_1603394430494.jpg" alt="Meghan and McPhee are longtime friends, having gone to school together and performed in musicals in Southern California"/><img src="https://i.dailymail.co.uk/i/furniture/mobile/icon_camera_90x68.png"/><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
Meghan and McPhee are longtime friends, having gone to school together and performed in musicals in Southern California
|
||||
</p><img src="https://i.dailymail.co.uk/1s/2020/10/22/20/31890530-8868177-Harry_and_Meghan_have_moved_into_the_star_studded_neighborhood_i-a-34_1603395073340.jpg" alt="Also in the star-studded neighborhood are Ellen DeGeneres, Oprah Winfrey, Rob Lowe and Gwyneth Paltrow"/><img src="https://i.dailymail.co.uk/i/furniture/mobile/icon_camera_90x68.png"/><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
Also in the star-studded neighborhood are Ellen DeGeneres, Oprah Winfrey, Rob Lowe and Gwyneth Paltrow
|
||||
</p><p>
|
||||
Foster, who is more than 30-years older than his wife, Harry and Meghan, is often referred to as a father figure for Prince Harry.
|
||||
</p><p>
|
||||
McPhee and Foster's baby will be her first child and his sixth.
|
||||
</p><p>
|
||||
Foster has five daughters and has been married five times: to B.J. Cook, Rebecca Dyer, Linda Thompson, Yolanda Hadid and McPhee.
|
||||
</p><p>
|
||||
The couple was married in London on June 28, 2019.
|
||||
</p><p>
|
||||
Another selling point of Montecito for Foster is he's a long time friend of Oprah Winfrey.
|
||||
</p><p>
|
||||
McPhee recently resumed filming on her upcoming Netflix series Country Comfort.
|
||||
</p><p>
|
||||
Last month the actress shared a photo of a masked table read with the cast, captioning it, 'and we're back!'
|
||||
</p><div>
|
||||
David Foster & Katherine McPhee, house hunting in Montecito
|
||||
</div><video preload="none" src="https://videos.dailymail.co.uk/video/mol/2020/10/20/4881903845281385283/640x360_MP4_4881903845281385283.mp4"><source src="https://videos.dailymail.co.uk/video/mol/2020/10/20/4881903845281385283/1024x576_MP4_4881903845281385283.mp4" type="video/mp4"/></video><img src="https://i.dailymail.co.uk/1s/2020/10/20/21/34632902-0-image-m-2_1603224136395.jpg"/><div>
|
||||
<span><span>Loaded</span>: 0%</span>
|
||||
</div><div>
|
||||
<span><span>Progress</span>: 0%</span>
|
||||
</div><div>
|
||||
<span>0:00</span>
|
||||
</div><div><span>+10</span>
|
||||
</div><ul><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yNy4zMjQgNDBoLTYuODc4di0xNi40OTZoLTMuNDM3di01LjY4NGgzLjQzN3YtMy40MTNjMC00LjYzOCAxLjk0LTcuMzk1IDcuNDU0LTcuMzk1aDQuNTl2NS42ODVoLTIuODdjLTIuMTQ2IDAtMi4yODguNzk0LTIuMjg4IDIuMjc3bC0uMDA4IDIuODQ2aDUuMTk4bC0uNjA4IDUuNjg0aC00LjU5eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0zNSAxMS42MDZjLS45ODUuNDQyLTIuMDQ1Ljc0LTMuMTU2Ljg3NSAxLjEzNS0uNjg3IDIuMDA2LTEuNzc1IDIuNDE2LTMuMDcyLTEuMDYyLjYzNi0yLjIzNyAxLjA5OS0zLjQ4OCAxLjM0OC0xLjAwMy0xLjA3OS0yLjQzMS0xLjc1My00LjAxMi0xLjc1My0zLjAzNCAwLTUuNDk0IDIuNDg2LTUuNDk0IDUuNTUzIDAgLjQzNS4wNDkuODU5LjE0MyAxLjI2Ni00LjU2Ny0uMjMyLTguNjE2LTIuNDQyLTExLjMyNi01LjgwMi0uNDczLjgyLS43NDQgMS43NzMtLjc0NCAyLjc5MiAwIDEuOTI2Ljk3IDMuNjI2IDIuNDQ1IDQuNjIyLS45MDEtLjAyOS0xLjc0OC0uMjc5LTIuNDg5LS42OTUtLjAwMS4wMjMtLjAwMS4wNDYtLjAwMS4wNyAwIDIuNjkgMS44OTQgNC45MzUgNC40MDggNS40NDQtLjQ2MS4xMjctLjk0Ni4xOTUtMS40NDcuMTk1LS4zNTQgMC0uNjk4LS4wMzUtMS4wMzQtLjEuNyAyLjIwNiAyLjcyOSAzLjgxMiA1LjEzMyAzLjg1Ni0xLjg4MSAxLjQ4OS00LjI1IDIuMzc3LTYuODI0IDIuMzc3LS40NDQgMC0uODgxLS4wMjYtMS4zMTEtLjA3NyAyLjQzMiAxLjU3NSA1LjMyIDIuNDk0IDguNDIzIDIuNDk0IDEwLjEwNyAwIDE1LjYzNC04LjQ2MSAxNS42MzQtMTUuNzk5IDAtLjI0MS0uMDA2LS40OC0uMDE3LS43MTkgMS4wNzMtLjc4MyAyLjAwNS0xLjc2MSAyLjc0MS0yLjg3NXoiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjQwIiB2aWV3Qm94PSIwIDAgNDAgNDAiIHdpZHRoPSI0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGRlZnM+PHJlY3QgaWQ9ImEiIGhlaWdodD0iNDAiIHJ4PSIyIiB3aWR0aD0iNDAiLz48bWFzayBpZD0iYiIgZmlsbD0iI2ZmZiIgaGVpZ2h0PSI0MCIgd2lkdGg9IjQwIiB4PSIwIiB5PSIwIj48dXNlIHhsaW5rOmhyZWY9IiNhIi8+PC9tYXNrPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0iIj48dXNlIG1hc2s9InVybCgjYikiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiB4bGluazpocmVmPSIjYSIvPjxwYXRoIGQ9Im0yMC42NjYgNmMtNy43NTQgMC0xMS42NjQgNS40NzktMTEuNjY0IDEwLjA0NyAwIDIuNzY2IDEuMDYyIDUuMjI4IDMuMzQyIDYuMTQ1LjM3NC4xNS43MDguMDA1LjgxNy0uNDAyLjA3Ni0uMjgzLjI1NC0uOTk0LjMzMy0xLjI5MS4xMDktLjQwMy4wNjctLjU0NS0uMjM1LS44OTYtLjY1Ny0uNzY0LTEuMDc3LTEuNzUzLTEuMDc3LTMuMTU0IDAtNC4wNjQgMy4wODUtNy43MDMgOC4wMzUtNy43MDMgNC4zODMgMCA2Ljc5IDIuNjM5IDYuNzkgNi4xNjQgMCA0LjYzOC0yLjA4MiA4LjU1MS01LjE3NCA4LjU1MS0xLjcwNyAwLTIuOTg1LTEuMzkxLTIuNTc1LTMuMDk4LjQ5LTIuMDM3IDEuNDQtNC4yMzYgMS40NC01LjcwNyAwLTEuMzE3LS43MTctMi40MTUtMi4yMDEtMi40MTUtMS43NDYgMC0zLjE0NyAxLjc4LTMuMTQ3IDQuMTYzIDAgMS41MTguNTIxIDIuNTQ1LjUyMSAyLjU0NXMtMS43ODYgNy40NTgtMi4wOTkgOC43NjRjLS42MjQgMi42MDItLjA5NCA1Ljc5MS0uMDQ5IDYuMTEyLjAyNi4xOS4yNzUuMjM2LjM4OC4wOTMuMTYxLS4yMDcgMi4yNC0yLjczNiAyLjk0Ni01LjI2NC4yLS43MTYgMS4xNDgtNC40MjEgMS4xNDgtNC40MjEuNTY3IDEuMDY1IDIuMjI0IDIuMDA1IDMuOTg2IDIuMDA1IDUuMjQ3IDAgOC44MDctNC43MTQgOC44MDctMTEuMDIzIDAtNC43NzItNC4xMDEtOS4yMTUtMTAuMzMyLTkuMjE1eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4="/><img src="data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMzEgMzIiIHdpZHRoPSIzMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMTEuNDE0IDE3LjMzMmMtMS4yMTEgMS4xOTMtMi44NjUgMS45MjctNC42ODcgMS45MjctMy43MTUgMC02LjcyNi0zLjA1MS02LjcyNi02LjgxNSAwLTMuNzY0IDMuMDEyLTYuODE1IDYuNzI2LTYuODE1IDIuMzA4IDAgNC4zNDUgMS4xNzggNS41NTYgMi45NzNsOS4zODUtMy45ODRjLS4wMTctLjE1NC0uMDI2LS4zMTEtLjAyNi0uNDcgMC0yLjI5MSAxLjgzMy00LjE0OCA0LjA5NC00LjE0OCAyLjI2MSAwIDQuMDk0IDEuODU3IDQuMDk0IDQuMTQ4cy0xLjgzMyA0LjE0OC00LjA5NCA0LjE0OGMtMS4yNjUgMC0yLjM5Ni0uNTgxLTMuMTQ3LTEuNDk0bC05LjM0MiAzLjk2NWMuMTM0LjUzNi4yMDUgMS4wOTguMjA1IDEuNjc3IDAgMS4wNTktLjIzOSAyLjA2Mi0uNjY0IDIuOTU3bDkuMTIgNi42MjZjLjk2MS0uODA0IDIuMTkyLTEuMjg2IDMuNTM1LTEuMjg2IDMuMDY5IDAgNS41NTcgMi41MiA1LjU1NyA1LjYzcy0yLjQ4OCA1LjYzLTUuNTU3IDUuNjNjLTMuMDY5IDAtNS41NTctMi41Mi01LjU1Ny01LjYzIDAtLjg4Ny4yMDMtMS43MjYuNTYzLTIuNDczeiIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09IiIvPjwvc3ZnPg=="/></ul><p>
|
||||
Since the Sussexes moved to Montecito, music producer Foster and McPhee have spent a lot of time there and fallen in love with the area
|
||||
</p><p>
|
||||
Another photo showed a monitor on set capturing her breaking into laughter with her costar Eddie Cibrian.
|
||||
</p><p>
|
||||
The two were announced to star in the series in January, but production was halted in March due to the coronavirus pandemic.
|
||||
</p><p>
|
||||
The Sussexes stepped down as senior working royals on March 31 and although they initially moved to Canada - living in a mansion Foster had set them up with for free -they have since set up home in California.
|
||||
</p><p>
|
||||
The couple first lived in Tyler Perry's sprawling $18 million hilltop mansion in Beverly Hills before buying a home in Montecito.
|
||||
</p></div>
|
||||
592
packages/readabilityjs/test/test-pages/danluu/distiller.html
Normal file
592
packages/readabilityjs/test/test-pages/danluu/distiller.html
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
<div><h4>Abstract</h4><p>
|
||||
Virtual reality (VR) is one of the most demanding human-in-the-loop
|
||||
applications from a latency standpoint. The latency between the physical
|
||||
movement of a user’s head and updated photons from a head mounted display
|
||||
reaching their eyes is one of the most critical factors in providing a
|
||||
high quality experience.
|
||||
</p><p>
|
||||
Human sensory systems can detect very small relative delays in parts of
|
||||
the visual or, especially, audio fields, but when absolute delays are
|
||||
below approximately 20 milliseconds they are generally imperceptible.
|
||||
Interactive 3D systems today typically have latencies that are several
|
||||
times that figure, but alternate configurations of the same hardware
|
||||
components can allow that target to be reached.
|
||||
</p><p>
|
||||
A discussion of the sources of latency throughout a system follows, along
|
||||
with techniques for reducing the latency in the processing done on the
|
||||
host system.
|
||||
</p><h4>Introduction</h4><p>
|
||||
Updating the imagery in a head mounted display (HMD) based on a head
|
||||
tracking sensor is a subtly different challenge than most human / computer
|
||||
interactions. With a conventional mouse or game controller, the user is
|
||||
consciously manipulating an interface to complete a task, while the goal
|
||||
of virtual reality is to have the experience accepted at an unconscious
|
||||
level.
|
||||
</p><p>
|
||||
Users can adapt to control systems with a significant amount of latency
|
||||
and still perform challenging tasks or enjoy a game; many thousands of
|
||||
people enjoyed playing early network games, even with 400+ milliseconds of
|
||||
latency between pressing a key and seeing a response on screen.
|
||||
</p><p>
|
||||
If large amounts of latency are present in the VR system, users may still
|
||||
be able to perform tasks, but it will be by the much less rewarding means
|
||||
of using their head as a controller, rather than accepting that their head
|
||||
is naturally moving around in a stable virtual world. Perceiving latency
|
||||
in the response to head motion is also one of the primary causes of
|
||||
simulator sickness. Other technical factors that affect the quality of a
|
||||
VR experience, like head tracking accuracy and precision, may interact
|
||||
with the perception of latency, or, like display resolution and color
|
||||
depth, be largely orthogonal to it.
|
||||
</p><p>
|
||||
A total system latency of 50 milliseconds will feel responsive, but still
|
||||
subtly lagging. One of the easiest ways to see the effects of latency in a
|
||||
head mounted display is to roll your head side to side along the view
|
||||
vector while looking at a clear vertical edge. Latency will show up as an
|
||||
apparent tilting of the vertical line with the head motion; the view feels
|
||||
“dragged along” with the head motion. When the latency is low enough, the
|
||||
virtual world convincingly feels like you are simply rotating your view of
|
||||
a stable world.
|
||||
</p><p>
|
||||
Extrapolation of sensor data can be used to mitigate some system latency,
|
||||
but even with a sophisticated model of the motion of the human head, there
|
||||
will be artifacts as movements are initiated and changed. It is always
|
||||
better to not have a problem than to mitigate it, so true latency
|
||||
reduction should be aggressively pursued, leaving extrapolation to smooth
|
||||
out sensor jitter issues and perform only a small amount of prediction.
|
||||
</p><h4>Data collection</h4><p>
|
||||
It is not usually possible to introspectively measure the complete system
|
||||
latency of a VR system, because the sensors and display devices external
|
||||
to the host processor make significant contributions to the total latency.
|
||||
An effective technique is to record high speed video that simultaneously
|
||||
captures the initiating physical motion and the eventual display update.
|
||||
The system latency can then be determined by single stepping the video and
|
||||
counting the number of video frames between the two events.
|
||||
</p><p>
|
||||
In most cases there will be a significant jitter in the resulting timings
|
||||
due to aliasing between sensor rates, display rates, and camera rates, but
|
||||
conventional applications tend to display total latencies in the dozens of
|
||||
240 fps video frames.
|
||||
</p><p>
|
||||
On an unloaded Windows 7 system with the compositing Aero desktop
|
||||
interface disabled, a gaming mouse dragging a window displayed on a 180 hz
|
||||
CRT monitor can show a response on screen in the same 240 fps video frame
|
||||
that the mouse was seen to first move, demonstrating an end to end latency
|
||||
below four milliseconds. Many systems need to cooperate for this to
|
||||
happen: The mouse updates 500 times a second, with no filtering or
|
||||
buffering. The operating system immediately processes the update, and
|
||||
immediately performs GPU accelerated rendering directly to the framebuffer
|
||||
without any page flipping or buffering. The display accepts the video
|
||||
signal with no buffering or processing, and the screen phosphors begin
|
||||
emitting new photons within microseconds.
|
||||
</p><p>
|
||||
In a typical VR system, many things go far less optimally, sometimes
|
||||
resulting in end to end latencies of over 100 milliseconds.
|
||||
</p><h4>Sensors</h4><p>
|
||||
Detecting a physical action can be as simple as a watching a circuit close
|
||||
for a button press, or as complex as analyzing a live video feed to infer
|
||||
position and orientation.
|
||||
</p><p>
|
||||
In the old days, executing an IO port input instruction could directly
|
||||
trigger an analog to digital conversion on an ISA bus adapter card, giving
|
||||
a latency on the order of a microsecond and no sampling jitter issues.
|
||||
Today, sensors are systems unto themselves, and may have internal
|
||||
pipelines and queues that need to be traversed before the information is
|
||||
even put on the USB serial bus to be transmitted to the host.
|
||||
</p><p>
|
||||
Analog sensors have an inherent tension between random noise and sensor
|
||||
bandwidth, and some combination of analog and digital filtering is usually
|
||||
done on a signal before returning it. Sometimes this filtering is
|
||||
excessive, which can contribute significant latency and remove subtle
|
||||
motions completely.
|
||||
</p><p>
|
||||
Communication bandwidth delay on older serial ports or wireless links can
|
||||
be significant in some cases. If the sensor messages occupy the full
|
||||
bandwidth of a communication channel, latency equal to the repeat time of
|
||||
the sensor is added simply for transferring the message. Video data
|
||||
streams can stress even modern wired links, which may encourage the use of
|
||||
data compression, which usually adds another full frame of latency if not
|
||||
explicitly implemented in a pipelined manner.
|
||||
</p><p>
|
||||
Filtering and communication are constant delays, but the discretely
|
||||
packetized nature of most sensor updates introduces a variable latency, or
|
||||
“jitter” as the sensor data is used for a video frame rate that differs
|
||||
from the sensor frame rate. This latency ranges from close to zero if the
|
||||
sensor packet arrived just before it was queried, up to the repeat time
|
||||
for sensor messages. Most USB HID devices update at 125 samples per
|
||||
second, giving a jitter of up to 8 milliseconds, but it is possible to
|
||||
receive 1000 updates a second from some USB hardware. The operating system
|
||||
may impose an additional random delay of up to a couple milliseconds
|
||||
between the arrival of a message and a user mode application getting the
|
||||
chance to process it, even on an unloaded system.
|
||||
</p><h4>Displays</h4><p>
|
||||
On old CRT displays, the voltage coming out of the video card directly
|
||||
modulated the voltage of the electron gun, which caused the screen
|
||||
phosphors to begin emitting photons a few microseconds after a pixel was
|
||||
read from the frame buffer memory.
|
||||
</p><p>
|
||||
Early LCDs were notorious for “ghosting” during scrolling or animation,
|
||||
still showing traces of old images many tens of milliseconds after the
|
||||
image was changed, but significant progress has been made in the last two
|
||||
decades. The transition times for LCD pixels vary based on the start and
|
||||
end values being transitioned between, but a good panel today will have a
|
||||
switching time around ten milliseconds, and optimized displays for active
|
||||
3D and gaming can have switching times less than half that.
|
||||
</p><p>
|
||||
Modern displays are also expected to perform a wide variety of processing
|
||||
on the incoming signal before they change the actual display elements. A
|
||||
typical Full HD display today will accept 720p or interlaced composite
|
||||
signals and convert them to the 1920×1080 physical pixels. 24 fps movie
|
||||
footage will be converted to 60 fps refresh rates. Stereoscopic input may
|
||||
be converted from side-by-side, top-down, or other formats to frame
|
||||
sequential for active displays, or interlaced for passive displays.
|
||||
Content protection may be applied. Many consumer oriented displays have
|
||||
started applying motion interpolation and other sophisticated algorithms
|
||||
that require multiple frames of buffering.
|
||||
</p><p>
|
||||
Some of these processing tasks could be handled by only buffering a single
|
||||
scan line, but some of them fundamentally need one or more full frames of
|
||||
buffering, and display vendors have tended to implement the general case
|
||||
without optimizing for the cases that could be done with low or no delay.
|
||||
Some consumer displays wind up buffering three or more frames internally,
|
||||
resulting in 50 milliseconds of latency even when the input data could
|
||||
have been fed directly into the display matrix.
|
||||
</p><p>
|
||||
Some less common display technologies have speed advantages over LCD
|
||||
panels; OLED pixels can have switching times well under a millisecond, and
|
||||
laser displays are as instantaneous as CRTs.
|
||||
</p><p>
|
||||
A subtle latency point is that most displays present an image
|
||||
incrementally as it is scanned out from the computer, which has the effect
|
||||
that the bottom of the screen changes 16 milliseconds later than the top
|
||||
of the screen on a 60 fps display. This is rarely a problem on a static
|
||||
display, but on a head mounted display it can cause the world to appear to
|
||||
shear left and right, or “waggle” as the head is rotated, because the
|
||||
source image was generated for an instant in time, but different parts are
|
||||
presented at different times. This effect is usually masked by switching
|
||||
times on LCD HMDs, but it is obvious with fast OLED HMDs.
|
||||
</p><h4>Host processing</h4><p>The classic processing model for a game or VR application is:</p><pre><code>Read user input -> run simulation -> issue rendering commands -> graphics drawing -> wait for vsync -> scanout
|
||||
|
||||
I = Input sampling and dependent calculation
|
||||
S = simulation / game execution
|
||||
R = rendering engine
|
||||
G = GPU drawing time
|
||||
V = video scanout time
|
||||
</code></pre><p>
|
||||
All latencies are based on a frame time of roughly 16 milliseconds, a
|
||||
progressively scanned display, and zero sensor and pixel latency.
|
||||
</p><p>
|
||||
If the performance demands of the application are well below what the
|
||||
system can provide, a straightforward implementation with no parallel
|
||||
overlap will usually provide fairly good latency values. However, if
|
||||
running synchronized to the video refresh, the minimum latency will still
|
||||
be 16 ms even if the system is infinitely fast. This rate feels good for
|
||||
most eye-hand tasks, but it is still a perceptible lag that can be felt in
|
||||
a head mounted display, or in the responsiveness of a mouse cursor.
|
||||
</p><pre><code>Ample performance, vsync:
|
||||
ISRG------------|VVVVVVVVVVVVVVVV|
|
||||
.................. latency 16 – 32 milliseconds
|
||||
</code></pre><p>
|
||||
Running without vsync on a very fast system will deliver better latency,
|
||||
but only over a fraction of the screen, and with visible tear lines. The
|
||||
impact of the tear lines are related to the disparity between the two
|
||||
frames that are being torn between, and the amount of time that the tear
|
||||
lines are visible. Tear lines look worse on a continuously illuminated LCD
|
||||
than on a CRT or laser projector, and worse on a 60 fps display than a 120
|
||||
fps display. Somewhat counteracting that, slow switching LCD panels blur
|
||||
the impact of the tear line relative to the faster displays.
|
||||
</p><p>
|
||||
If enough frames were rendered such that each scan line had a unique
|
||||
image, the effect would be of a “rolling shutter”, rather than visible
|
||||
tear lines, and the image would feel continuous. Unfortunately, even
|
||||
rendering 1000 frames a second, giving approximately 15 bands on screen
|
||||
separated by tear lines, is still quite objectionable on fast switching
|
||||
displays, and few scenes are capable of being rendered at that rate, let
|
||||
alone 60x higher for a true rolling shutter on a 1080P display.
|
||||
</p><pre><code>Ample performance, unsynchronized:
|
||||
ISRG
|
||||
VVVVV
|
||||
..... latency 5 – 8 milliseconds at ~200 frames per second
|
||||
</code></pre><p>
|
||||
In most cases, performance is a constant point of concern, and a parallel
|
||||
pipelined architecture is adopted to allow multiple processors to work in
|
||||
parallel instead of sequentially. Large command buffers on GPUs can buffer
|
||||
an entire frame of drawing commands, which allows them to overlap the work
|
||||
on the CPU, which generally gives a significant frame rate boost at the
|
||||
expense of added latency.
|
||||
</p><pre><code>CPU:ISSSSSRRRRRR----|
|
||||
GPU: |GGGGGGGGGGG----|
|
||||
VID: | |VVVVVVVVVVVVVVVV|
|
||||
.................................. latency 32 – 48 milliseconds
|
||||
</code></pre><p>
|
||||
When the CPU load for the simulation and rendering no longer fit in a
|
||||
single frame, multiple CPU cores can be used in parallel to produce more
|
||||
frames. It is possible to reduce frame execution time without increasing
|
||||
latency in some cases, but the natural split of simulation and rendering
|
||||
has often been used to allow effective pipeline parallel operation. Work
|
||||
queue approaches buffered for maximum overlap can cause an additional
|
||||
frame of latency if they are on the critical user responsiveness path.
|
||||
</p><pre><code>CPU1:ISSSSSSSS-------|
|
||||
CPU2: |RRRRRRRRR-------|
|
||||
GPU : | |GGGGGGGGGG------|
|
||||
VID : | | |VVVVVVVVVVVVVVVV|
|
||||
.................................................... latency 48 – 64 milliseconds
|
||||
</code></pre><p>
|
||||
Even if an application is running at a perfectly smooth 60 fps, it can
|
||||
still have host latencies of over 50 milliseconds, and an application
|
||||
targeting 30 fps could have twice that. Sensor and display latencies can
|
||||
add significant additional amounts on top of that, so the goal of 20
|
||||
milliseconds motion-to-photons latency is challenging to achieve.
|
||||
</p><h4>Latency Reduction Strategies</h4><h4>Prevent GPU buffering</h4><p>
|
||||
The drive to win frame rate benchmark wars has led driver writers to
|
||||
aggressively buffer drawing commands, and there have even been cases where
|
||||
drivers ignored explicit calls to glFinish() in the name of improved
|
||||
“performance”. Today’s fence primitives do appear to be reliably observed
|
||||
for drawing primitives, but the semantics of buffer swaps are still
|
||||
worryingly imprecise. A recommended sequence of commands to synchronize
|
||||
with the vertical retrace and idle the GPU is:
|
||||
</p><pre><code>SwapBuffers();
|
||||
DrawTinyPrimitive();
|
||||
InsertGPUFence();
|
||||
BlockUntilFenceIsReached();
|
||||
</code></pre><p>
|
||||
While this should always prevent excessive command buffering on any
|
||||
conformant driver, it could conceivably fail to provide an accurate
|
||||
vertical sync timing point if the driver was transparently implementing
|
||||
triple buffering.
|
||||
</p><p>
|
||||
To minimize the performance impact of synchronizing with the GPU, it is
|
||||
important to have sufficient work ready to send to the GPU immediately
|
||||
after the synchronization is performed. The details of exactly when the
|
||||
GPU can begin executing commands are platform specific, but execution can
|
||||
be explicitly kicked off with glFlush() or equivalent calls. If the code
|
||||
issuing drawing commands does not proceed fast enough, the GPU may
|
||||
complete all the work and go idle with a “pipeline bubble”. Because the
|
||||
CPU time to issue a drawing command may have little relation to the GPU
|
||||
time required to draw it, these pipeline bubbles may cause the GPU to take
|
||||
noticeably longer to draw the frame than if it were completely buffered.
|
||||
Ordering the drawing so that larger and slower operations happen first
|
||||
will provide a cushion, as will pushing as much preparatory work as
|
||||
possible before the synchronization point.
|
||||
</p><pre><code>Run GPU with minimal buffering:
|
||||
CPU1:ISSSSSSSS-------|
|
||||
CPU2: |RRRRRRRRR-------|
|
||||
GPU : |-GGGGGGGGGG-----|
|
||||
VID : | |VVVVVVVVVVVVVVVV|
|
||||
................................... latency 32 – 48 milliseconds
|
||||
</code></pre><p>
|
||||
Tile based renderers, as are found in most mobile devices, inherently
|
||||
require a full scene of command buffering before they can generate their
|
||||
first tile of pixels, so synchronizing before issuing any commands will
|
||||
destroy far more overlap. In a modern rendering engine there may be
|
||||
multiple scene renders for each frame to handle shadows, reflections, and
|
||||
other effects, but increased latency is still a fundamental drawback of
|
||||
the technology.
|
||||
</p><p>
|
||||
High end, multiple GPU systems today are usually configured for AFR, or
|
||||
Alternate Frame Rendering, where each GPU is allowed to take twice as long
|
||||
to render a single frame, but the overall frame rate is maintained because
|
||||
there are two GPUs producing frames
|
||||
</p><pre><code>Alternate Frame Rendering dual GPU:
|
||||
CPU1:IOSSSSSSS-------|IOSSSSSSS-------|
|
||||
CPU2: |RRRRRRRRR-------|RRRRRRRRR-------|
|
||||
GPU1: | GGGGGGGGGGGGGGGGGGGGGGGG--------|
|
||||
GPU2: | | GGGGGGGGGGGGGGGGGGGGGGG---------|
|
||||
VID : | | |VVVVVVVVVVVVVVVV|
|
||||
.................................................... latency 48 – 64 milliseconds
|
||||
</code></pre><p>
|
||||
Similarly to the case with CPU workloads, it is possible to have two or
|
||||
more GPUs cooperate on a single frame in a way that delivers more work in
|
||||
a constant amount of time, but it increases complexity and generally
|
||||
delivers a lower total speedup.
|
||||
</p><p>
|
||||
An attractive direction for stereoscopic rendering is to have each GPU on
|
||||
a dual GPU system render one eye, which would deliver maximum performance
|
||||
and minimum latency, at the expense of requiring the application to
|
||||
maintain buffers across two independent rendering contexts.
|
||||
</p><p>
|
||||
The downside to preventing GPU buffering is that throughput performance
|
||||
may drop, resulting in more dropped frames under heavily loaded
|
||||
conditions.
|
||||
</p><h4>Late frame scheduling</h4><p>
|
||||
Much of the work in the simulation task does not depend directly on the
|
||||
user input, or would be insensitive to a frame of latency in it. If the
|
||||
user processing is done last, and the input is sampled just before it is
|
||||
needed, rather than stored off at the beginning of the frame, the total
|
||||
latency can be reduced.
|
||||
</p><p>
|
||||
It is very difficult to predict the time required for the general
|
||||
simulation work on the entire world, but the work just for the player’s
|
||||
view response to the sensor input can be made essentially deterministic.
|
||||
If this is split off from the main simulation task and delayed until
|
||||
shortly before the end of the frame, it can remove nearly a full frame of
|
||||
latency.
|
||||
</p><pre><code>Late frame scheduling:
|
||||
CPU1:SSSSSSSSS------I|
|
||||
CPU2: |RRRRRRRRR-------|
|
||||
GPU : |-GGGGGGGGGG-----|
|
||||
VID : | |VVVVVVVVVVVVVVVV|
|
||||
.................... latency 18 – 34 milliseconds
|
||||
</code></pre><p>
|
||||
Adjusting the view is the most latency sensitive task; actions resulting
|
||||
from other user commands, like animating a weapon or interacting with
|
||||
other objects in the world, are generally insensitive to an additional
|
||||
frame of latency, and can be handled in the general simulation task the
|
||||
following frame.
|
||||
</p><p>
|
||||
The drawback to late frame scheduling is that it introduces a tight
|
||||
scheduling requirement that usually requires busy waiting to meet, wasting
|
||||
power. If your frame rate is determined by the video retrace rather than
|
||||
an arbitrary time slice, assistance from the graphics driver in accurately
|
||||
determining the current scanout position is helpful.
|
||||
</p><h4>View bypass</h4><p>
|
||||
An alternate way of accomplishing a similar, or slightly greater latency
|
||||
reduction Is to allow the rendering code to modify the parameters
|
||||
delivered to it by the game code, based on a newer sampling of user input.
|
||||
</p><p>
|
||||
At the simplest level, the user input can be used to calculate a delta
|
||||
from the previous sampling to the current one, which can be used to modify
|
||||
the view matrix that the game submitted to the rendering code.
|
||||
</p><p>
|
||||
Delta processing in this way is minimally intrusive, but there will often
|
||||
be situations where the user input should not affect the rendering, such
|
||||
as cinematic cut scenes or when the player has died. It can be argued that
|
||||
a game designed from scratch for virtual reality should avoid those
|
||||
situations, because a non-responsive view in a HMD is disorienting and
|
||||
unpleasant, but conventional game design has many such cases.
|
||||
</p><p>
|
||||
A binary flag could be provided to disable the bypass calculation, but it
|
||||
is useful to generalize such that the game provides an object or function
|
||||
with embedded state that produces rendering parameters from sensor input
|
||||
data instead of having the game provide the view parameters themselves. In
|
||||
addition to handling the trivial case of ignoring sensor input, the
|
||||
generator function can incorporate additional information such as a
|
||||
head/neck positioning model that modified position based on orientation,
|
||||
or lists of other models to be positioned relative to the updated view.
|
||||
</p><p>
|
||||
If the game and rendering code are running in parallel, it is important
|
||||
that the parameter generation function does not reference any game state
|
||||
to avoid race conditions.
|
||||
</p><pre><code>View bypass:
|
||||
CPU1:ISSSSSSSSS------|
|
||||
CPU2: |IRRRRRRRRR------|
|
||||
GPU : |--GGGGGGGGGG----|
|
||||
VID : | |VVVVVVVVVVVVVVVV|
|
||||
.................. latency 16 – 32 milliseconds
|
||||
</code></pre><p>
|
||||
The input is only sampled once per frame, but it is simultaneously used by
|
||||
both the simulation task and the rendering task. Some input processing
|
||||
work is now duplicated by the simulation task and the render task, but it
|
||||
is generally minimal.
|
||||
</p><p>
|
||||
The latency for parameters produced by the generator function is now
|
||||
reduced, but other interactions with the world, like muzzle flashes and
|
||||
physics responses, remain at the same latency as the standard model.
|
||||
</p><p>
|
||||
A modified form of view bypass could allow tile based GPUs to achieve
|
||||
similar view latencies to non-tiled GPUs, or allow non-tiled GPUs to
|
||||
achieve 100% utilization without pipeline bubbles by the following steps:
|
||||
</p><p>
|
||||
Inhibit the execution of GPU commands, forcing them to be buffered. OpenGL
|
||||
has only the deprecated display list functionality to approximate this,
|
||||
but a control extension could be formulated.
|
||||
</p><p>
|
||||
All calculations that depend on the view matrix must reference it
|
||||
independently from a buffer object, rather than from inline parameters or
|
||||
as a composite model-view-projection (MVP) matrix.
|
||||
</p><p>
|
||||
After all commands have been issued and the next frame has started, sample
|
||||
the user input, run it through the parameter generator, and put the
|
||||
resulting view matrix into the buffer object for referencing by the draw
|
||||
commands.
|
||||
</p><p>Kick off the draw command execution.</p><pre><code>Tiler optimized view bypass:
|
||||
CPU1:ISSSSSSSSS------|
|
||||
CPU2: |IRRRRRRRRRR-----|I
|
||||
GPU : | |-GGGGGGGGGG-----|
|
||||
VID : | | |VVVVVVVVVVVVVVVV|
|
||||
.................. latency 16 – 32 milliseconds
|
||||
</code></pre><p>
|
||||
Any view frustum culling that was performed to avoid drawing some models
|
||||
may be invalid if the new view matrix has changed substantially enough
|
||||
from what was used during the rendering task. This can be mitigated at
|
||||
some performance cost by using a larger frustum field of view for culling,
|
||||
and hardware clip planes based on the culling frustum limits can be used
|
||||
to guarantee a clean edge if necessary. Occlusion errors from culling,
|
||||
where a bright object is seen that should have been occluded by an object
|
||||
that was incorrectly culled, are very distracting, but a temporary clean
|
||||
encroaching of black at a screen edge during rapid rotation is almost
|
||||
unnoticeable.
|
||||
</p><h4>Time warping</h4><p>
|
||||
If you had perfect knowledge of how long the rendering of a frame would
|
||||
take, some additional amount of latency could be saved by late frame
|
||||
scheduling the entire rendering task, but this is not practical due to the
|
||||
wide variability in frame rendering times.
|
||||
</p><pre><code>Late frame input sampled view bypass:
|
||||
CPU1:ISSSSSSSSS------|
|
||||
CPU2: |----IRRRRRRRRR--|
|
||||
GPU : |------GGGGGGGGGG|
|
||||
VID : | |VVVVVVVVVVVVVVVV|
|
||||
.............. latency 12 – 28 milliseconds
|
||||
</code></pre><p>
|
||||
However, a post processing task on the rendered image can be counted on to
|
||||
complete in a fairly predictable amount of time, and can be late scheduled
|
||||
more easily. Any pixel on the screen, along with the associated depth
|
||||
buffer value, can be converted back to a world space position, which can
|
||||
be re-transformed to a different screen space pixel location for a
|
||||
modified set of view parameters.
|
||||
</p><p>
|
||||
After drawing a frame with the best information at your disposal, possibly
|
||||
with bypassed view parameters, instead of displaying it directly, fetch
|
||||
the latest user input, generate updated view parameters, and calculate a
|
||||
transformation that warps the rendered image into a position that
|
||||
approximates where it would be with the updated parameters. Using that
|
||||
transform, warp the rendered image into an updated form on screen that
|
||||
reflects the new input. If there are two dimensional overlays present on
|
||||
the screen that need to remain fixed, they must be drawn or composited in
|
||||
after the warp operation, to prevent them from incorrectly moving as the
|
||||
view parameters change.
|
||||
</p><pre><code>Late frame scheduled time warp:
|
||||
CPU1:ISSSSSSSSS------|
|
||||
CPU2: |RRRRRRRRRR----IR|
|
||||
GPU : |-GGGGGGGGGG----G|
|
||||
VID : | |VVVVVVVVVVVVVVVV|
|
||||
.... latency 2 – 18 milliseconds
|
||||
</code></pre><p>
|
||||
If the difference between the view parameters at the time of the scene
|
||||
rendering and the time of the final warp is only a change in direction,
|
||||
the warped image can be almost exactly correct within the limits of the
|
||||
image filtering. Effects that are calculated relative to the screen, like
|
||||
depth based fog (versus distance based fog) and billboard sprites will be
|
||||
slightly different, but not in a manner that is objectionable.
|
||||
</p><p>
|
||||
If the warp involves translation as well as direction changes, geometric
|
||||
silhouette edges begin to introduce artifacts where internal parallax
|
||||
would have revealed surfaces not visible in the original rendering. A
|
||||
scene with no silhouette edges, like the inside of a box, can be warped
|
||||
significant amounts and display only changes in texture density, but
|
||||
translation warping realistic scenes will result in smears or gaps along
|
||||
edges. In many cases these are difficult to notice, and they always
|
||||
disappear when motion stops, but first person view hands and weapons are a
|
||||
prominent case. This can be mitigated by limiting the amount of
|
||||
translation warp, compressing or making constant the depth range of the
|
||||
scene being warped to limit the dynamic separation, or rendering the
|
||||
disconnected near field objects as a separate plane, to be composited in
|
||||
after the warp.
|
||||
</p><p>
|
||||
If an image is being warped to a destination with the same field of view,
|
||||
most warps will leave some corners or edges of the new image undefined,
|
||||
because none of the source pixels are warped to their locations. This can
|
||||
be mitigated by rendering a larger field of view than the destination
|
||||
requires; but simply leaving unrendered pixels black is surprisingly
|
||||
unobtrusive, especially in a wide field of view HMD.
|
||||
</p><p>
|
||||
A forward warp, where source pixels are deposited in their new positions,
|
||||
offers the best accuracy for arbitrary transformations. At the limit, the
|
||||
frame buffer and depth buffer could be treated as a height field, but
|
||||
millions of half pixel sized triangles would have a severe performance
|
||||
cost. Using a grid of triangles at some fraction of the depth buffer
|
||||
resolution can bring the cost down to a very low level, and the trivial
|
||||
case of treating the rendered image as a single quad avoids all silhouette
|
||||
artifacts at the expense of incorrect pixel positions under translation.
|
||||
</p><p>
|
||||
Reverse warping, where the pixel in the source rendering is estimated
|
||||
based on the position in the warped image, can be more convenient because
|
||||
it is implemented completely in a fragment shader. It can produce
|
||||
identical results for simple direction changes, but additional artifacts
|
||||
near geometric boundaries are introduced if per-pixel depth information is
|
||||
considered, unless considerable effort is expended to search a
|
||||
neighborhood for the best source pixel.
|
||||
</p><p>
|
||||
If desired, it is straightforward to incorporate motion blur in a reverse
|
||||
mapping by taking several samples along the line from the pixel being
|
||||
warped to the transformed position in the source image.
|
||||
</p><p>
|
||||
Reverse mapping also allows the possibility of modifying the warp through
|
||||
the video scanout. The view parameters can be predicted ahead in time to
|
||||
when the scanout will read the bottom row of pixels, which can be used to
|
||||
generate a second warp matrix. The warp to be applied can be interpolated
|
||||
between the two of them based on the pixel row being processed. This can
|
||||
correct for the “waggle” effect on a progressively scanned head mounted
|
||||
display, where the 16 millisecond difference in time between the display
|
||||
showing the top line and bottom line results in a perceived shearing of
|
||||
the world under rapid rotation on fast switching displays.
|
||||
</p><h4>
|
||||
Continuously updated time warping
|
||||
</h4><p>
|
||||
If the necessary feedback and scheduling mechanisms are available, instead
|
||||
of predicting what the warp transformation should be at the bottom of the
|
||||
frame and warping the entire screen at once, the warp to screen can be
|
||||
done incrementally while continuously updating the warp matrix as new
|
||||
input arrives.
|
||||
</p><pre><code>Continuous time warp:
|
||||
CPU1:ISSSSSSSSS------|
|
||||
CPU2: |RRRRRRRRRRR-----|
|
||||
GPU : |-GGGGGGGGGGGG---|
|
||||
WARP: | W| W W W W W W W W|
|
||||
VID : | |VVVVVVVVVVVVVVVV|
|
||||
... latency 2 – 3 milliseconds for 500hz sensor updates
|
||||
</code></pre><p>
|
||||
The ideal interface for doing this would be some form of “scanout shader”
|
||||
that would be called “just in time” for the video display. Several video
|
||||
game systems like the Atari 2600, Jaguar, and Nintendo DS have had buffers
|
||||
ranging from half a scan line to several scan lines that were filled up in
|
||||
this manner.
|
||||
</p><p>
|
||||
Without new hardware support, it is still possible to incrementally
|
||||
perform the warping directly to the front buffer being scanned for video,
|
||||
and not perform a swap buffers operation at all.
|
||||
</p><p>
|
||||
A CPU core could be dedicated to the task of warping scan lines at roughly
|
||||
the speed they are consumed by the video output, updating the time warp
|
||||
matrix each scan line to blend in the most recently arrived sensor
|
||||
information.
|
||||
</p><p>
|
||||
GPUs can perform the time warping operation much more efficiently than a
|
||||
conventional CPU can, but the GPU will be busy drawing the next frame
|
||||
during video scanout, and GPU drawing operations cannot currently be
|
||||
scheduled with high precision due to the difficulty of task switching the
|
||||
deep pipelines and extensive context state. However, modern GPUs are
|
||||
beginning to allow compute tasks to run in parallel with graphics
|
||||
operations, which may allow a fraction of a GPU to be dedicated to
|
||||
performing the warp operations as a shared parameter buffer is updated by
|
||||
the CPU.
|
||||
</p><h4>Discussion</h4><p>
|
||||
View bypass and time warping are complementary techniques that can be
|
||||
applied independently or together. Time warping can warp from a source
|
||||
image at an arbitrary view time / location to any other one, but artifacts
|
||||
from internal parallax and screen edge clamping are reduced by using the
|
||||
most recent source image possible, which view bypass rendering helps
|
||||
provide.
|
||||
</p><p>
|
||||
Actions that require simulation state changes, like flipping a switch or
|
||||
firing a weapon, still need to go through the full pipeline for 32 – 48
|
||||
milliseconds of latency based on what scan line the result winds up
|
||||
displaying on the screen, and translational information may not be
|
||||
completely faithfully represented below the 16 – 32 milliseconds of the
|
||||
view bypass rendering, but the critical head orientation feedback can be
|
||||
provided in 2 – 18 milliseconds on a 60 hz display. In conjunction with
|
||||
low latency sensors and displays, this will generally be perceived as
|
||||
immediate. Continuous time warping opens up the possibility of latencies
|
||||
below 3 milliseconds, which may cross largely unexplored thresholds in
|
||||
human / computer interactivity.
|
||||
</p><p>
|
||||
Conventional computer interfaces are generally not as latency demanding as
|
||||
virtual reality, but sensitive users can tell the difference in mouse
|
||||
response down to the same 20 milliseconds or so, making it worthwhile to
|
||||
apply these techniques even in applications without a VR focus.
|
||||
</p><p>
|
||||
A particularly interesting application is in “cloud gaming”, where a
|
||||
simple client appliance or application forwards control information to a
|
||||
remote server, which streams back real time video of the game. This offers
|
||||
significant convenience benefits for users, but the inherent network and
|
||||
compression latencies makes it a lower quality experience for action
|
||||
oriented titles. View bypass and time warping can both be performed on the
|
||||
server, regaining a substantial fraction of the latency imposed by the
|
||||
network. If the cloud gaming client was made more sophisticated, time
|
||||
warping could be performed locally, which could theoretically reduce the
|
||||
latency to the same levels as local applications, but it would probably be
|
||||
prudent to restrict the total amount of time warping to perhaps 30 or 40
|
||||
milliseconds to limit the distance from the source images.
|
||||
</p><h4>Acknowledgements</h4><p>Zenimax for allowing me to publish this openly.</p><p>Hillcrest Labs for inertial sensors and experimental firmware.</p><p>Emagin for access to OLED displays.</p><p>Oculus for a prototype Rift HMD.</p><p>
|
||||
Nvidia for an experimental driver with access to the current scan line
|
||||
number.
|
||||
</p></div>
|
||||
264
packages/readabilityjs/test/test-pages/danwang/distiller.html
Normal file
264
packages/readabilityjs/test/test-pages/danwang/distiller.html
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
<div><p>
|
||||
<em>(This piece is my year in review; here’s my letter from</em> <a href="https://danwang.co/2019-letter/" target="_blank" rel="noreferrer noopener"><em>2019</em></a><em>)</em>
|
||||
</p><p>
|
||||
<strong>I. Inspiration</strong>
|
||||
</p><p>
|
||||
It’s difficult to identify a great economic reason to explore space. There are easier ways to extract minerals, doing anything at all is terribly expensive, and Mars is a hard place to make a living. The benefits of space exploration are instead mostly inspirational. Few other human activities are so grand to captivate the imagination, and doing these uneconomic projects have pulled forward technological capabilities that may otherwise have languished.
|
||||
</p><p>
|
||||
It’s difficult to identify a great economic reason to practice socialism. Its historical results have ranged from catastrophic misallocation of talent at best to mass deaths at worst. But socialism still retains appeal to broad segments of many populations, which shows that it has considerable inspirational value. For better or for worse, there are still many advocates for the creation of some form of a more equal society.
|
||||
</p><p>
|
||||
This year, I read every issue of Qiushi (translation: Seeking Truth), the party’s flagship theory journal, whose core task is to spell out the evolving idea of socialism with Chinese characteristics. For those not familiar, Qiushi reads like a cross between the New Yorker and the Federal Register. Published twice a month, the magazine features lengthy essays, thick pages, and some of the finest writers in the party. Each issue starts in the same way: a reprint of a speech or essay by Xi Jinping—in a font distinct from the rest of the magazine’s—and then commentary and reports from the rest of the party state. Accompanying pictures feature either the country’s leaders making inspections, scenes of the people, or major pieces of infrastructure and heavy industry.
|
||||
</p><p>
|
||||
Its audience? People with nothing better to do than read the party center’s commentary (like retired cadres), or those who are keenly interested in Beijing’s priorities, like local officials. Reading party speeches with its various annexes and cross references echoes my main professional activity these days. That is the study of the US sanctions regime—namely Commerce’s Export Administration Regulations and Treasury’s IEEPA-based authorities. Party speeches and US regulations are both made up of arcane, formal language that make references to more obscure texts, which themselves hint at still more distant and terrible truths. US sanctions lawyers, I suspect, can have a splendid time with Qiushi.
|
||||
</p><p>
|
||||
Steady engagement with the journal throughout the year has forced me to think more deeply about the Chinese Communist Party. There are many things that Xi wants to do, I believe that his most fundamental goal is to make this Marxist-Leninist party an effective governing force for the present century. His patient work to reshape the bureaucracy is aided by a distinctive feature of the Chinese system: the use of propaganda to create centralized campaigns of inspiration. Some of Xi’s efforts have borne fruit: the country’s governance capabilities have markedly improved, a trend that is observable in daily life. At the same time, the state has grown much more repressive. A focus on repression shouldn’t neglect the improvement in the country’s institutional and commercial strengths; and appreciation of this improvement ought to be tempered by the party center’s growing mania for control.
|
||||
</p><p>
|
||||
When foreign commentators discuss the experience of reading state media, they rarely fail to attach a reference to its “turgid prose.” While some partyspeak is indeed unreadable, I’ve always seen that dismissal as a signal of contempt for the party’s pronouncements, thus deterring people from taking it seriously. But there is reason to treat its content with care. Propaganda might not matter to you, but it matters to the party. Anne-Marie Brady has pointed out
|
||||
</p><div>that the leadership considers propaganda to be the “lifeblood” of the party state. Propaganda work is considered so powerful that the person in charge must be only a functionary. Brady shows that the head of propaganda always has a seat on the Politburo, but shouldn’t usually be allowed to reach the standing committee. He is not to be too imaginative, or he might dominate the entire political system. Propaganda is key to understanding the party, since it governs not in itself, but in symbiosis with state institutions. For the most part, the party’s role can be boiled down to two items: <em>inspiration</em>, by setting the ideological direction, and <em>control</em>, through its power to select personnel.
|
||||
</div><p>
|
||||
Qiushi offers an authoritative articulation of the central government’s priorities at any moment. Its job, like the rest of the state media, consists of repetition and explication of a few phrases. It’s easy to roll one’s eyes at crude sloganeering, like the two centenary goals of achieving a “moderately prosperous society in all respects” by 2021 and “a modern socialist country and the great rejuvenation of the Chinese people” by 2049. But the need to fix slogans makes good sense in Chinese governance: the party center has to speak to all local officials as well as the entire population. As Richard Epstein has argued, the greater the complexity in a system, the simpler the rules that govern it must be. One should allow, for example, extensive and nuanced bargaining between buyer and seller at the vegetable stall, but for an online marketplace to manage millions of transactions a day, then its rules must be very simple indeed.
|
||||
</p><p>
|
||||
It’s up to the party center to adjust and refine slogans to signal the priorities of the moment. The easiest way to appreciate the importance of that effort is to consider Deng Xiaoping’s efforts to shift the country to pragmatic governance through Reform and Opening (itself a named initiative). He invented or instrumentalized a series of ingenious phrases that include “development is the only hard truth,” “cross the river by feeling the stones,” and “practice is the sole criterion for the determination of truth.” My favorite is his declaration at the 13th party congress that China is in the “primary stage of socialism.” Left unspecified is how long this stage will last and how many more stages there will be before the people can enjoy the full deal.
|
||||
</p><p>
|
||||
Many of the party center’s slogans tend to be deliberately vague, thus permitting lower-level officials to figure out implementation. My one-sentence definition of Xi Jinping Thought on Socialism with Chinese Characteristics for a New Era is: “To achieve the two centenary goals—under the leadership of the party—by accomplishing tasks that include but are not limited to eliminating poverty, advancing the socialist rule of law, improving party discipline, etc.” That concept is still an evolving one. Meanwhile, we await better explication of the hot slogans this year: “dual circulation” and “demand-side reform.” Some of it can be absurd: I’m skeptical that anyone can readily explain the nuances between “rule of law,” “socialist rule of law,” and “socialist rule of law with Chinese characteristics.”
|
||||
</p><p>
|
||||
When it’s not being vague, the party can be trying to have things both ways. Xi declared at the third plenum in 2013 that market forces would have a “decisive” role in allocating resources, while at the same time the state sector would have a “leading” role. It’s not unusual to see a great deal of semantic acrobatics. Deng declared that socialism means the capacity to concentrate resources to accomplish great tasks; under that definition, the Apollo and Manhattan projects were socialism. In July, Xi reminded us that “socialism with Chinese characteristics has many distinctive features, but its most essential is leadership by the Chinese Communist Party.”
|
||||
</p><div>In other words, socialism with Chinese characteristics means the party is never wrong. Either the market or the state sector can be more important at any moment: it is the party’s pleasure to decide.
|
||||
</div><p>
|
||||
Centralized campaigns of inspiration, which usually manifests through fixing slogans, is a distinctive feature of the Chinese political system. In the US, political candidates trot out slogans when they run for election; in China, one is never far from the next big named initiative. At its best, defining major goals is the essence of political leadership, and nowhere is this principle better illustrated than Apollo. John F. Kennedy announced the target in 1961: land a man on the moon and return him safely to earth before the decade was out. By fixing this clear goal,
|
||||
</p><div>as well as committing the necessary spending, he accelerated the creation, development, and deployment of technologies that made the lunar landings possible.
|
||||
</div><p>
|
||||
Xi grasps this idea of leadership. In his tenure, he has unleashed a torrent of new initiatives. In my view, he feels that the practice of governing China under socialism cannot be an exercise in sustained mendacity. The political system can no longer continue to be an unstable structure based on ad hoc compromises; instead it must have a clear organizational structure, with the party at the top. And the ruling party needs to have the political consciousness of an effective governing force.
|
||||
</p><p>
|
||||
Consider two of his most important initiatives: the campaign against corruption and the move toward law-based governance. Xi has decided that corruption is not a mystery to be endured, but a problem to be solved. A few years past the peak of the crackdown, it’s fair to say that the campaign hasn’t solely been effective in removing his adversaries, but has also been broad enough to restore some degree of public confidence in government. A few commentators contend that removal of opportunities for graft have prompted talented people to leave government. But the flip side of that coin has been the improvement in morale among the civil servants who found corruption among colleagues to be intolerable, and can finally see themselves doing public work well.
|
||||
</p><p>
|
||||
And for years, Xi has emphasized following clear rules of written procedure, under the rubric of “law-based governance.”
|
||||
</p><div>Since then, the state has improved regulatory systems, for example in setting clear standards for license approvals and in securities and antitrust regulation. The state has removed some of the arbitrary aspects of governance, thus bringing serious enforcement actions following the passage of relatively clear regulations. That has improved facts on the ground. Companies and lawyers tell me that a decade-long effort by the State Council to ease doing business has yielded real results. Obtaining business licenses no longer requires a relentless pace of wining and dining, and has instead become close to a matter of routine. I haven’t been able to verify this fact for myself, but one of my friends told me that the office of the National Development and Reform Commission used to be ringed by some of the fanciest restaurants in Beijing, offering mostly private rooms; many of these restaurants have now closed, following the professionalization of business approvals.
|
||||
</div><p>
|
||||
The lived experience of being in Beijing has improved in parallel. I remember what a nightmare it was to buy a high-speed rail ticket for the first time years ago, which involved lots of yelling and multiple people cutting in line. Today, I purchase one on my phone, with no need to obtain a paper ticket, and the lines to board are more or less orderly. Consumer products of all sorts are getting pretty good, and the customer service experience of engaging with any of these companies tends to be not unpleasant. There’s certainly still all sorts of disorderly behavior, but anyone can notice the improvement on all fronts of daily life. In more macro view, some of the breathless stories from years ago on China’s growing capabilities look at last like they’re on good ground today: the country has produced credible companies, have many investable assets (especially in fixed income), and are building globally-competitive brands.
|
||||
</p><p>
|
||||
It’s easy to enumerate the grave problems facing the country, but critics tend to under-appreciate its strengths. Chief among them, in my view, has been the party’s surprising adaptability. At any given point, commentators have said that the problems have become too big for the government to handle. Meanwhile, the country has achieved a good record of pulling itself out of sticky situations: in 1992 when it restarted reform, after the financial crisis of 1997, and again in 2008. That record was validated most spectacularly again this year in the aftermath of the Covid-19 outbreak.
|
||||
</p><p>
|
||||
This year made me believe that China is the country with the most can-do spirit in the world. Every segment of society mobilized to contain the pandemic. One manufacturer expressed astonishment to me at how slowly western counterparts moved. US companies had to ask whether making masks aligned with the company’s core competence. Chinese companies simply decided that making money is their core competence, and therefore they should be making masks. The State Council reported that between March and May, China exported 70 billion masks and nearly 100,000 ventilators.
|
||||
</p><div>Some of these masks had problems early on, but the manufacturers learned and fixed them or were culled by regulatory action, and China’s exports were able to grow when no one else could restart production. Soon enough, masks were big enough to be seen in the export data.
|
||||
</div><p>
|
||||
It’s obvious that the authorities in Wuhan screwed up big, but it’s also the case that the central government organized an effective response to virus containment. It’s not just the manufacturers: the consumer internet companies leapt into action in a way that their US peers did not.
|
||||
</p><div>Francis Fukuyama states that high-trust societies have “spontaneous sociability,” in which people are able to organize more quickly, initiate action, and sacrifice for the common good. On each of these metrics, I submit that China should receive high marks.
|
||||
</div><p>
|
||||
As every discussion on China grows more strident, and as every proposition about it has to be vested with sentiment, I submit that it’s all the more important to be able to see things as they are. That entails having coming to terms not just with a rise of its repressive capabilities, but also with its growing commercial and institutional strengths. US elites have abandoned the idea that China would liberalize nicely. They should put another idea to bed: that this authoritarian system, riddled with weaknesses, is on the brink of collapse. The country’s strengths are real and improving while the government becomes more nasty towards its critics and the rest of the world.
|
||||
</p><p>
|
||||
China is neither a Marxist fundamentalist regime nor a universally-surveilled open-air prison, in which one is free to do nothing but worship the party and carry out its edicts. That is however the impression created by quite a bit of the media.
|
||||
</p><div>I think that’s not the fault of individual journalists, instead more structural explanations are at work. News bureaus are highly concentrated in Beijing, due in part to natural corporate consolidation, but mostly because the government maintains a strict cap on foreign journalist visas. As a result, the bulk of journalists are based in the part of China that has the most politics and the least sense of growth. Everything here is doom and gloom, a fact well conveyed to the outside world. What’s missing are the facts of more pleasant life and higher growth in other cities. In an ideal world, it should not be crazy to imagine that the papers should have correspondents based in places that include places like Chongqing, Hangzhou, or Xiamen, all of which have interesting stories to tell.
|
||||
</div><p>
|
||||
Xi is preparing to face a more challenging international environment with a raft of initiatives. He has consistently said over the last few years that “we must handle our own affairs well.” That has meant building up domestic capabilities while not lashing out against American firms. He has also invoked history to strengthen morale in the party. 2021 is the centenary of the party’s founding, and the major slogan of the past two years has been: “Remember where we started from, pursue our destiny, the struggle is forever.”
|
||||
</p><p>
|
||||
Given the importance of the slogan, it’s worthwhile to try to come to terms with the fondness and reverence his generation has for the party’s early days. Many of the people tormented by the party center, including Deng and Xi’s father, have ended up being fiercely loyal to the party.
|
||||
</p><div>That shows not just that human nature is complex, but also that the revolutionary heritage of the party instills pride. The CCP started out as a combat party constantly at the mercy of forces grander than itself, achieving its goals after an unusually long struggle that repeatedly brought it to the brink of death. Daniel Koss reminds us that the longer that revolutionary parties have to struggle before consolidating power, the more stronger their ideological commitments and the greater their governance durability tend to be.
|
||||
</div><p>
|
||||
Xi is keen to reflect upon the regime’s history. He has decided that the party must believe in itself, and that it is correct to do so: “If our Party members and officials are firm in their ideals and convictions and maintain high morale in their activities and initiatives, and if our people are high-spirited and determined, then we will surely create many miracles.”
|
||||
</p><div>Furthermore, he has stated: “The prospects are bright but the challenges are severe. All comrades must aim high and look far, be alert to dangers even in times of calm, have the courage to pursue reform and break new ground, and never become hardened to change.”
|
||||
</div><p>
|
||||
Thus I’ve arrived at the idea that a commitment to centralized campaigns of inspiration, represented by the tendency to fix clear goals, is the booster stage required to leave the gravitational pull of decadence and complacency. Ross Douthat laments that “a consistent ineffectuality in American governance is just the way things are.”
|
||||
</p><div>And he references Jacques Barzun, who defines a decadent society as one that is “peculiarly restless, for it sees no clear lines of advance.” As a society turns developed, its main problems become social: an organizational sclerosis, which no technology is sophisticated enough to solve. No great effort is required to identify the comprehensive paralysis in the US. And that is the political and social current that Xi is trying to reverse in China.
|
||||
</div><p>
|
||||
One way to do that is to continue to pursue GDP growth, which has mostly become an unfashionable idea today in the west. Xi reminded the state in July that “economic work must be our core task, if we succeed in that, then the rest of our tasks become easy.”
|
||||
</p><div>Barry Naughton has noted that “China’s system of incentives for local bureaucrats to encourage growth is extremely unusual, and seems only to exist in China. It is a blunt and powerful instrument.”
|
||||
</div><p>
|
||||
This emphasis on growth makes it less likely for China to develop into American complacency or decadence. There are other types of paralysis that it stands a good chance of avoiding. With its emphasis on the real economy, it is trying to avoid the fate of Hong Kong, where local elites have reorganized the productive forces completely around sustaining high property prices and managing mainland liquidity flows. With its emphasis on economic growth, it cannot be like Taiwan, whose single bright corporate beacon is surrounded by a mass of firms undergoing genteel decline. With its emphasis on manufacturing, it cannot be like the UK, which is so successful in the sounding-clever industries—television, journalism, finance, and universities—while seeing a falling share of R&D intensity and a global loss of standing among its largest firms.
|
||||
</p><p>
|
||||
Douthat’s book does not deal seriously with China, only with a fantasy of a universally-surveilled society under the rubric of a social credit system. If he did engage more seriously, he might pick up what Frank Pieke has termed “neo-socialism,” which is the attempt to harness market liberalization to strengthen state capacity and a more Leninist party.
|
||||
</p><div>In return, the state provides purpose and direction, as well as inspiring the rest of society with a transformative mission. It helps, of course, that Xi is a genuine believer in socialism, which to him is both an instrument as well as an end. He’s leveraging that belief to reject decadence and assert agency to point out new lines of advance.
|
||||
</div><p>
|
||||
<strong>II. Control</strong>
|
||||
</p><p>
|
||||
That was quite a lot of theory. Where does it fall apart?
|
||||
</p><p>
|
||||
Xi has said: “If we turn a blind eye to challenges, or even dodge or disguise them; if we fear to advance in the face of challenges and sit by and watch the unfolding calamity; then they will grow beyond our control and cause irreparable damage.”
|
||||
</p><div>Instead of heeding this warning, authorities in Wuhan suppressed reporting of a spread of a novel virus. At a time when they should have imposed restrictions, they congregated thousands around a gigantic potluck. That has indeed unfolded into a calamity.
|
||||
</div><p>
|
||||
Xi has said<strong>:</strong> “Some officials are perfunctory in their work, shirking responsibility when troubles come and dodging thorny problems. They like to report every trifle to their superiors for approval or directives. In doing so, they appear to be abiding by the rules but are actually avoiding responsibilities. Some make ill-considered or purely arbitrary decisions. They place themselves above the party organization and allow no dissenting voices.”
|
||||
</p><div> But as economic growth slows down, the country is doubling down on centralized government. Over the last several years, the state is taking more of a leading role in the economy, which means a larger role for bureaucrats.
|
||||
</div><p>
|
||||
Xi has said: “Self-criticism needs to be specific about our problems and needs to touch underlying questions… We must be gratified when told of our errors; we must not shy away from our shortcomings. We must accommodate different opinions and sharp criticism.”
|
||||
</p><div>When medical professionals spoke up about a strange new virus circulating in Wuhan, police gave them reprimands. More and more often, the state is simply arresting critics. Even though the government has every reason to be confident about the effectiveness of its virus containment, it has issued a jail sentence to a citizen journalist under the catch-all charge of “picking quarrels and provoking trouble.” For all the emphasis on seeking truth from facts, the state still maintains this practice of shooting the messenger or jailing its critics.
|
||||
</div><p>
|
||||
On its own terms, the party center’s instruction is unevenly followed. And there are plenty of reasons to doubt the sustainability of Chinese growth that exist beyond the party’s capacity for self-reform. The following have all received extensive treatment: demographics will be a clear and serious drag in only a few years; an uncomfortable buildup of debt is now accompanied by growing investor discomfort with strategic defaults; the environment is bearing greater stresses; and based on the state’s aggression abroad and the operation of detention camps for minority groups at home, the rest of the world has become much less friendly towards China. One can add more items here, I want to consider the problems with centralized campaigns of inspiration.
|
||||
</p><p>
|
||||
The creation and repetition of key slogans isn’t just crowding out the room for other ideas. The state has prosecuted a decade-long effort to suppress the views it doesn’t like. Not only has the government ramped up censorship, society as a whole is developing greater intolerance for dissenting ideas.
|
||||
</p><p>
|
||||
It’s difficult to draw a clear line from tighter speech restrictions to worse economic outcomes. Greater censorship over the last decade has coincided with still-impressive levels of economic growth as well as the growing competitiveness of many more companies. And I think it’s worth considering that the authoritarianism of the late-Prussian and early-German state coincided with the creation of the modern research university as well as fantastic advances in chemistry, physics, and electrical engineering.
|
||||
</p><p>
|
||||
But there’s more on-the-ground evidence that ordinary people are growing nervous. In so many settings, one has to tread on eggshells in a public discussion in China, with organizers taking pains to remind audience members of sensitivities. Sometimes even in private, people beg off with an embarrassed laugh that they can’t discuss a subject due to unspecified difficulties. WeChat blocks sensitive keywords, which today includes “decoupling” and “sanctions.” It’s now inconvenient to use the app for professional conversations, and I’ve been pretty insistent to my contacts to use Signal instead. And since I brought up Germany, I wonder if the right analogy for China today is as a successful East Germany.
|
||||
</p><p>
|
||||
It’s hard to imagine that this increasingly censorious environment is conducive to good thinking. Actions from the government seem to be matched by a growing intolerance among the population for dissenting views. That’s due in part to their sense of feeling besieged after international opinion on China turned sharply negative after the virus outbreak. That hasn’t made it any better for Fang Fang, the novelist in Wuhan whose journal entries documenting the pandemic were first widely-read and then widely-criticized after she authorized an English translation. At that point, critics charged her with “blackening China’s name” and “handing a knife to China’s enemies.” The abuse wasn’t confined online: prominent personalities in state media have led criticism campaigns against her. I wonder if this society can be reflective and thus capable of self-improvement if it is so intolerant of criticism.
|
||||
</p><p>
|
||||
It might not be clear that censoriousness is hurting the creation of new companies, but it is clear that it’s becoming more difficult to create better cultural products. Over the last decade, China’s most successful cultural exports include TikTok, the Three-Body Problem, a few art house films (mostly directed by Jia Zhangke), and that might be it. The Three-Body Problem was published in 2008 and translated into English in 2014; today, the series looks more like something that was able to escape the system rather than the vanguard of a great Chinese outpouring of marvelous cultural creations. Not content to allow science fiction movies to develop independently, the film authorities have this year released guidelines on the correct ideological direction of new films.
|
||||
</p><div>Films more broadly are facing censorship. The two blockbusters released this year (Guan Hu’s Eight Hundred and Zhang Yimou’s One Second) were both mysteriously pulled from festivals and released to the public this year after the state demanded edits.
|
||||
</div><p>
|
||||
My best cultural experience this year was to see the Met’s production of Porgy and Bess. It is one of America’s greatest dramas: the story of the marginalized community of Catfish Row, written by an outsider whose works defy easy categorization, and featuring music of surpassing beauty. As I watched a production with superb voices and exuberant dancers, in one of the most lavish theaters in the world, I wondered whether China might one day produce a story of such power. Or if instead every new work must encapsulate core socialist values and the spirit of the 19th party congress.
|
||||
</p><p>
|
||||
This lack of compelling cultural creations matters for many reasons. One of them is that people who’ve never been able to make a visit cannot really visualize the life of an ordinary Chinese person, only the dystopia that has become the way that most foreigners think about the country. The propaganda department has not only failed to directly create globally-appealing culture, it has regulated private creative efforts out of existence. For all of Xi’s hopes to “tell China’s story well,” the Chinese regime seems congenitally incapable of allowing good stories about itself to be told, because of its obsession with exercising total control.
|
||||
</p><p>
|
||||
For most of the last few decades, the state has not been so repressive as to smother the most dynamic elements of the economy and society. And I think it is still intent on controlling a limited number of areas it has determined to be political threats (and that it will do very strictly). But every few months brings greater risk that dynamism decays from the shrinking space for acceptable speech and thought. The direction of travel has not been a happy one. As recently as five years into Xi’s term, there were still optimists who believed that his regime might turn out to be more kind. The removal of term limits routed that camp, and few recent events can re-instill confidence that the state sees a limit to greater repression. Detention camps have not gone away, and I wonder if they will be expanded to more than a few sites in western provinces. Surveillance capabilities have significantly scaled up. And everyone knows that the regime is serious about instilling discipline and control.
|
||||
</p><p>
|
||||
There has been a more obvious way that the state has set back leading companies this year: through greater assertiveness abroad. The Ministry of Foreign Affairs has peddled inflammatory and sometimes false claims, angering other countries. These acts are contributing to the steady closure of developed markets to Chinese firms. Local ambassadors and official spokespeople have sometimes threatened to halt key projects or cut off the Chinese market to major companies. After economic threats against countries like Australia, the foreign ministry is strengthening arguments among national security hawks in the west that countries should be less dependent on China. In some ways, Chinese diplomats have become the greatest threats to Chinese exporters.
|
||||
</p><p>
|
||||
<strong>III. A clear line of advance</strong>
|
||||
</p><p>
|
||||
Meanwhile, the state is growing more deeply involved in the economy, especially in technology. This has been a pivotal year for China and tech, which will be a good area for observing the party’s tactics on inspiration at work. A relentless pace of US actions targeting Chinese companies has delivered unmistakable setbacks to their operations. That has triggered a whole-of-society response to build up domestic technology capabilities, an effort that will be guided at the highest political level.
|
||||
</p><p>
|
||||
I wrote <a href="https://danwang.co/2019-letter/" target="_blank" rel="noreferrer noopener">last year</a>: “The US responded to the rise of the USSR and Japan by focusing on innovation; it’s early days, but so far the US is responding to the technological rise of China by kneecapping its leading firms. So instead of realizing its own Sputnik moment, it is triggering one in China.”
|
||||
</p><p>
|
||||
This year, the US doubled down. It produced two rounds of novel restrictions on Huawei, threatened wider restrictions on Tencent and ByteDance, forced the sale of TikTok to a US consortium, and limited technology exports on SMIC, DJI, and dozens of other companies. Aside from Alibaba, it’s hard to name many big Chinese tech firms that have not faced sanctions or the threat of one from the US.
|
||||
</p><p>
|
||||
The actual effects of these regulatory actions have been uneven. Designation to the entity list hasn’t always had a major impact on every company’s operations. Federal courts have tied up the bans on Tencent’s WeChat and ByteDance’s TikTok. At the same time, Huawei is trying to work through major difficulties, especially in its smartphone business. TikTok, China’s most successful tech export, still might be sold off. And more generally, Chinese firms are starting to be locked out of developed markets. Lack of access to the richest and most discerning consumers makes it more difficult to make the best products in the world.
|
||||
</p><p>
|
||||
The US can revel in Huawei’s pain. But its actions have not been costless to itself. By withholding components that Chinese companies have relied upon, the US government has turned American firms into unreliable suppliers. These restrictions can sometimes block non-American firms from making sales too. In an extraordinary assertion of extraterritoriality, the US declared in August that any company, anywhere in the world, needs to apply for a license to sell a product to Huawei if it is produced on the basis of US technologies.
|
||||
</p><p>
|
||||
Nothing can be easier to destroy than trust. Chinese companies have responded by de-Americanizing their supply chains because they have no choice. US politicians can observe the sometimes-devastating impacts of sanctions. What they don’t seem to realize—or want to believe—is that they’re simultaneously pummeling the American brand writ large. I’ve documented for Dragonomics the uncomfortable questions American companies tell me they’re starting to face on whether they can credibly be long-term suppliers. Elsewhere, the Economist has reported that even poultry farmers in China are wondering if they’ll be able to import baby chicks from the US.
|
||||
</p><div>And there are now multiple reported instances of Japanese companies marketing themselves as more reliable than their American competitors.
|
||||
</div><div>Moreover, I hear growing unease from companies in the rest of Asia and Europe on buying American. Can everyone really be sure that this denial campaign will be limited to a handful of bad Chinese actors? Or is a better model of the US government that once it has found a fun new toy, it will keep playing with it until it is no longer fun?
|
||||
</div><p>
|
||||
With these regulations, the US has initiated one of the greatest and strangest antitrust actions ever, against potentially all American exporters. The US Treasury has for years expressed worry about the potential decline of the dollar’s dominance following excessive use of blocking sanctions. This fear is turning into reality for the real economy. One might expect alarm bells to be going off in DC, but it doesn’t appear that there’s much pushback against these regulations, except for murmurs from trade associations. It’s possible to defend these moves as correct—for example by justifying that the costs on American firms are worth it for the chance to slow Huawei down right now—but the government does not appear to have had a vigorous debate about the tradeoffs. Instead, the strategy seems to be a result of bureaucratic kludges, pushed forward by whichever faction has the upper hand, made mostly because the financial sanctions office has resisted dealing a serious blow to Huawei in a single stroke.
|
||||
</p><p>
|
||||
For the most part, the control hawks faction of the government has had a run of the table, shown by the fact that US agencies have been more focused on taking down Chinese firms than extending US strengths. At a time when it’s more important than ever to advance its semiconductor companies, the government is crippling their sales to their largest or fastest-growing market. When research capabilities at US universities need to grow, the government is denying them students. And when the US should be attracting more talent to its shores, the government has made it more difficult for people to immigrate. Thus the US looks committed to a strategy to destroy the scientific and industrial establishment in order to save it.
|
||||
</p><p>
|
||||
Meanwhile in China, these actions have triggered a surge of interest in mastering technology. For the first time arguably since the industrial rise of Japan in the 1950s, a major country is committed to thinking deeply about the invention of its own tooling. A whole generation of scientists and engineers must examine foundational problems like to build leading tools (like lithography machines) and create the best materials (like wafers and chemicals). And the state is fully behind that effort. After steady calls from Xi throughout the year to master technology, the Central Economic Work Conference announced in December that science and technology work will be the top priority in 2021; the conference has never broken science and technology out as an independent item, never mind give it top spot.
|
||||
</p><p>
|
||||
I wrote a <a href="https://www.bloomberg.com/opinion/articles/2020-12-18/u-s-sanctions-against-china-will-fuel-xi-jinping-s-tech-ambitions" target="_blank" rel="noreferrer noopener">column</a> on what a gamechanger these actions can be for Chinese industrial policy. Hardly any of China’s largest technology companies have escaped some form or threat of US sanctions, and many more are wondering if they will end up on some poorly-understood blacklist. Thus the US government has aligned the interests of China’s leading tech companies with the state’s interest in self-sufficiency and technological greatness. Huawei, the greatest victim of US actions, is now in the position of NASA in the 1960s when it comes to chips: a cash-rich entity willing to purchase on the basis of performance, not cost. Access to leading and demanding customers can give a chance to local suppliers who never would have had a shot competing against well-established American firms.
|
||||
</p><p>
|
||||
US restrictions are setting back Chinese companies in the short term, but I think it’s unlikely they can crush the broader effort to catch up. No country has monopolized a key technology forever: instead, the history of technology has mostly been a history of diffusion. And Chinese firms are hardly starting from a base of zero. The country has demonstrated a growing ability to master most industrial products and is doing well enough in digital technologies. It remains a dynamic market with a good and improving base of human talent. And perhaps <a href="https://danwang.co/how-technology-grows/" target="_blank" rel="noreferrer noopener">most importantly</a>, it is where most manufacturing is done today, which means its workers have the greatest exposure to technological learning. These advantages don’t guarantee success, especially not on a short timeline. But there’s a chance that things improve rather quickly. The natural trajectory of many technologies were pulled forward with unexpected speed after Kennedy announced his moon target.
|
||||
</p><p>
|
||||
Is it a drag that the state is so involved in this effort? Well, yes, and China might well repeat the industrial policy mistakes that have stymied projects in the past. But the state doesn’t feel like it can afford to be hands off. Commentators who criticize China’s efforts as doubling down on a state-led approach seem not to realize that the world has fundamentally changed in the last few years. First, the US cannot credibly guarantee to sell goods that Chinese firms need. And second, US actions have removed the political room that Chinese companies have had to push back against state demands that companies buy domestic. Apart from the processor, a Huawei phone is using comparable amounts of Chinese hardware as the iPhone. ByteDance, Alibaba, and Tencent have been using the best-in-class software and hardware, which are usually American. The state will have an easier time now enlisting these companies to use alternatives.
|
||||
</p><p>
|
||||
While promoting the status of science and technology with one hand, the Chinese government has with its other hand reined in the activities of consumer internet companies. I’ve never stopped lamenting the marketing trick that California pulled off to situate consumer internet as the highest form of technology, as if Tencent and Facebook are the surest signs that we live a technologically-accelerating civilization. The “tech” giants are highly-capable companies that print cash. But they’re barely engaged in the creation of intellectual property, excelling instead on business-model innovation and the exploitation of network effects. It’s become apparent in the last few months that the Chinese leadership has moved towards the view that hard tech is more valuable than products that take us more deeply into the digital world. Xi declared this year that while digitization is important, “we must recognize the fundamental importance of the real economy… and never deindustrialize.”
|
||||
</p><div>This expression preceded the passage of securities and antitrust regulations, thus also pummeling finance, which along with tech make up the most glamorous sectors today. The optimistic scenario is that these actions compress the wage and status premia of the internet and finance sectors, such that we’ll see fewer CVs that read: “BS Microelectronics, Peking; software engineer, Airbnb” or “PhD Applied Mathematics, Princeton; VP, Citibank.”
|
||||
</div><p>
|
||||
While China is ahead on this attitude shift, I think the US is starting to undergo the same conversion. On this theme, I think it’s worth reflecting on the Chinese actions that drove out Google and Facebook a decade ago. That move blocked a major market to these companies, potentially depriving them of significant revenues, and effectively split the world into two internets. And it has since become part of the justification for US actions against China’s technology champions. But US actions have been an order of magnitude more severe: by attacking Huawei’s supply chain, it can terminate the operations of the entire company, and thus represents a massive escalation. In any case, the Chinese ban of Facebook today looks like a prescient action, given how much the company’s activities have enraged western governments, who complain about the circulation of conspiracy theories and other misinformation on social media platforms. It’s harder to argue that China was foolish to ban products so wondrous that their CEOs need to be hauled on a regular basis before political leaders to endure demands to fix the social problems their platforms are allegedly amplifying.
|
||||
</p><p>
|
||||
Instead of coddling the internet companies, Xi has declared that China must never deindustrialize or lose its manufacturing capabilities. There’s some chance that Chinese scientists and engineers never make the breakthroughs that free them from dependence on foreign supply. But I think it’s unlikely that they completely fail: they only need to re-invent certain wheels, which does not mean dreaming up unheard-of new technologies. In the worst case, Chinese scientists engage in pure re-invention, making up for the technologies that they can no longer buy. The optimistic scenario is that they’ll find new ways of doing things after re-examining established ideas. The retreading of old paths might reveal vistas that were passed over too quickly, and which might offer new rewards once properly explored.
|
||||
</p><p>
|
||||
Whatever its other worries, the party leadership doesn’t have to fret about becoming a decadent society. Given that its per-capita GDP is still around a sixth of US levels, it still has substantial room for catch-up growth. And it won’t lack for identifying clear lines of advance. On top of eliminating poverty and saving the environment, the party can now add the goal to master technology. The leadership has already <a href="https://twitter.com/danwwang/status/1317964399013851136" target="_blank" rel="noreferrer noopener">held</a> collective study sessions on topics that include artificial intelligence and quantum computing. But it’s going to need to come up with some new tricks to inspire people in science and the industrial world.
|
||||
</p><p>
|
||||
There’s nothing like a good space project to captivate the imagination. I find it remarkable how little we discuss the fact that there are almost certainly warm oceans on the <a href="https://europa.nasa.gov/europa/faq/" target="_blank" rel="noreferrer noopener">moons of Jupiter</a> and Saturn which presents the possibility that we find extraterrestrial life within our very own solar system. Imagine the inspirational value that can flow from fixing a target, say the year of the 2049 centenary, to land a probe on these moons to explore for life.
|
||||
</p><p>
|
||||
<em>I thank a number of people for reading a draft of this section or discussing the core ideas with me.</em>
|
||||
</p><p>
|
||||
***
|
||||
</p><p>
|
||||
It’s time to talk about books.
|
||||
</p><p>
|
||||
This year, I read re-Proust. The first time I did so was during college, this second time was more rewarding. I learned to skip the most tedious parts: the endless descriptions of the French countryside, our narrator’s torment over his love of Albertine (and Gilberte, and Mme. de Guermantes), and how much he’s looking forward to seeing Venice. Instead, I focused on the descriptions of the personalities, for every shade of human vanity is depicted in these pages.
|
||||
</p><p>
|
||||
The novel describes the most intense lovesuffering, caused by suspicion, which are exacerbated by the most stupid mistakes, caused by pride. Much of the story is taken up by the effects of mad jealousies, which grip every major character in turn, with the same destructive effects on each. On this reading, I was struck by how much the novel is useful as a book of ethics. Many scenes easily resonate today, like how fiercely Mme. Verdurin or Mme. de Villeparisis must beg and threaten for people to show up for their little parties. It’s not just our narrator who is exposed to be acting ridiculously. Outrageous behavior from every character (driven by pride, vanity, or ambition) receives careful treatment and then a comprehensive skewering.
|
||||
</p><p>
|
||||
The key to reading Proust is not to pay too much attention to the plot. It’s of no great import, and one has to get used to abrupt shifts. In this way the novel is like Moby-Dick, which can shift from the politics of dining at Ahab’s table to a loving tour of the literal interior of a sperm whale’s head. Couldn’t find the transition? No matter, that detracts not at all from the wonderfulness of the scenes. Focus instead on the humor. There are many funny things that take place in the aristocratic set pieces, such as the constant misunderstandings of M. de Charlus at the dinner of the Verdurins, or his suspicion at the violinist who professes to enjoy solving algebra equations until late into the evenings, or his interactions with the Duc de Guermantes. Really anything with Charlus portends comedy.
|
||||
</p><p>
|
||||
Not everyone loves Proust’s sentences. I thought that <a href="https://amzn.to/38Qhmc5" target="_blank" rel="noreferrer noopener">Penguin’s translation</a> made them enchanting. In between the humor and the yearning, an air of melancholy is never distant, which gives the books extra depth. The ending is especially sad. Our narrator, whom we knew as a boy and then a young man, suddenly becomes quite old in the second half of the final volume. The novel reaches its climax with a lengthy and beautiful epiphany, like in Mann’s Magic Mountain, in which our narrator realizes a fierce urgency to write this book.
|
||||
</p><p>
|
||||
This was a good year for reading long books. In the early months of the pandemic, I went through Jürgen Osterhammel’s <a rel="noreferrer noopener" href="https://amzn.to/3pD3GrK" target="_blank"><strong>The Transformation of the World</strong></a>, a history of the nineteenth century. It’s chock full of facts and too difficult to summarize. In science fiction, I enjoyed Neal Stephenson’s <a rel="noreferrer noopener" href="https://amzn.to/3b0FEmq" target="_blank"><strong>Anathem</strong></a>. Stephenson has an amazing ability to locate all the nerd pleasure centers in one’s brain and then jam his fingers hard on them. Anathem is a bit of a twist on Stephenson’s usual trick: instead of presenting loving rewrites of his favorite Wikipedia articles, he serves us instead loving rewrites from the Stanford Encyclopedia of Philosophy, all wrapped up as usual in a delicious plot. (I want to thank my friend Thijs not only for insisting that I read the book but for mailing me his copy from Amsterdam.)
|
||||
</p><p>
|
||||
I continued my Christmas tradition of reading about the second world war. This year: <a href="https://amzn.to/3pGTEpC" target="_blank" rel="noreferrer noopener"><strong>Britain’s War</strong></a> by Daniel Todman. The British perspective is interesting for its focus on the empire: having to think about the colonies was a distraction to the British leadership, but the ability to draw upon disparate resources and produce goods from multiple base areas was a major factor in victory. I never find scenes from battle to be so interesting. Instead, the pleasure I draw from war books is to think through the logistical efforts involved in producing goods and delivering them to the front. The best war books treat these as mathematical problems. And they dwell on the bureaucratic effort involved in working these problems out.
|
||||
</p><p>
|
||||
Todman is good on the bureaucratic side of war: “In 1942, there were 1,850 admissions to hospital per 1,000 troops on the Burmese front.” And he allows one to get inside the planning effort: “The average round trip from the UK to North America took two months and twenty days; that from the UK to the Indian Ocean area, seven and a half months. These rhythms dominated planning and the pace of the war.” Logistics has always been an underrated discipline, especially now in the time of vaccine distribution. And pacing in personal life is something we should all be more actively thinking about. It’s not enough to have a big goal far out in the future, success requires identifying milestones and achieving them at a steady pace.
|
||||
</p><p>
|
||||
<a href="https://amzn.to/3o9Dk0i" target="_blank" rel="noreferrer noopener"><strong>Mao’s Third Front</strong></a> by Covell Meyskens is an account of the effort to make China undefeatable in the ‘60s. How? By relocating heavy industry from the coast into mountainous Sichuan. During the worst years of the Sino-Soviet split, Mao imagined that he might have to fend off an invasion from the revisionist Soviets as well as from the imperialist Americans, who had started to deploy troops in Vietnam. The Third Front was a colossal undertaking that wasn’t even meant as a deterrent, since the state didn’t publicize the effort. Instead, Mao was committed to doing the equivalent of moving industry to Siberia during peacetime, ready to retreat into the mountains (again), to be able to re-emerge victorious. The party’s history is still worth dwelling upon. Its big initiatives since the founding of the People’s Republic in 1949 include not just the Third Front, but also the Great Leap, transforming the written script, the Cultural Revolution, Reform and Opening, and the one-child policy. Any one of these would be a once-in-a-generation trauma, the party managed to pull them off at a rate of once a decade.
|
||||
</p><p>
|
||||
Virginia Tufte’s <a href="https://amzn.to/3rJwD7d" target="_blank" rel="noreferrer noopener"><strong>Artful Sentences: Syntax as Style</strong></a> is a useful compendium of different types of sentences. It’s good to keep around as a reference work. <a href="https://amzn.to/3aZAkQ8" target="_blank" rel="noreferrer noopener"><strong>Essays One</strong></a>, by Lydia Davis, and Tufte are the best writing references that I’ve recently read. Reading Davis prompts reflection, for example when she points to Flaubert’s description of style as “the rhythm of poetry, the precision of the language of science, capable of sustained melody like a cello; a style that would enter the mind like a stiletto and on which our thought could travel like a boat over calm water on a breeze.”
|
||||
</p><p>
|
||||
It’s a happy development that many more people are writing, especially in newsletter form. For now, I find quite a lot of internet writing to be difficult to read: no real sense of pacing, an inability to turn a phrase or sustain a metaphor, the <em>excessive</em> use of italics. Picking up Tufte and Davis would help. Starting internet essays feels too often like going to battle inside a trench, which produces the same sense of trepidation. Writers should prosecute instead a war of movement, conducted through bold and decisive strokes, concluding with an unmistakable impression upon the victim. The feeling that I strive for is to create tightly-controlled expressions of exuberance, like the dancepieces of Philip Glass, which propel a lot of my writing.
|
||||
</p><p>
|
||||
***
|
||||
</p><p>
|
||||
I may not not have accomplished much in life, but I’m proud at least to have eaten thalis in Chennai, pizza in Naples, and mie goreng in Singapore.
|
||||
</p><p>
|
||||
I know that Beijing is not the world’s best food city, but it might be the best food city for me. One can grab expensive sushi at the restaurant favored by the Japanese embassy or walk a few blocks and order five plates of dumplings for $20. One can find decent dosas, lots of Thai food, and even a bagel store whose breads would be out of place on the Upper West Side but would not be in San Francisco. Best of all, every region of China is represented in this city. To deal with the various challenges of a pandemic year, I found solace in stuffing my face.
|
||||
</p><p>
|
||||
I managed to sample dishes from all the provinces this year, including the relatively obscure cuisines from places like Anhui, Guangxi, and Jiangxi. My favorites are: Shanghai, Sichuan, and Yunnan.
|
||||
</p><p>
|
||||
Many people dislike Shanghai food—which I’m defining as the broader region that encompasses Suzhou, Hangzhou, Nanjing, etc.—for being too sweet. In my mind, it’s unquestionably the finest cuisine. Not only is it the best at the high end, its noodles, soups, and soup dumplings make up some of the tastiest casual food as well. It’s the cuisine that varies most by season, e.g. bamboo shoots in spring and mitten crabs in fall, which showcases the bountiness of the region and its emphasis on freshness. (That’s quite unlike the tradition of the north, which celebrates every and any occasion with plates of dumplings.) The mixing of vinegar and hot fat produces a slight, magical sweetness, and that is something that the Shanghainese understand well, along with many other secrets.
|
||||
</p><p>
|
||||
I expect that everyone is familiar with the glories of Sichuan food, there’s little that I need to add here. I’ve eaten plenty in Chengdu and Chongqing, I hope next that I can explore some of the villages in the countryside that feature local specialties.
|
||||
</p><p>
|
||||
And I hesitate to say that Yunnan is next best because it has become so trendy. Some people question whether Yunnan food is coherent enough to be a cuisine, or whether it’s a useless label for dishes that vary over a huge and mountainous province. I think of it as Chinese cooking styles with Southeast Asian ingredients, featuring dishes like rice noodles, which can be more soft or more chewy than wheat noodles, served in a mutton broth and topped with a generous fistful of fresh mint. There are many things one can find there that are uncommon in the rest of the country, like cheeses. My favorites are the mushrooms: there’s nothing more appealing than some freshly-picked mushrooms stir fried with bits of Yunnan ham.
|
||||
</p><p>
|
||||
Any of these three regions are worth traveling to for a food tour. My candidate for an underrated cuisine is the food of the northeast, which features breads and stews of huge proportions. I haven’t had enough exposure to foods of all the interior provinces, but I’m happy to suggest that the cuisines of Jiangxi and Anhui are worth exploring. And the category of highly-rated and correctly-rated cuisines should include the foods of the northwest (breads and noodles), Hunan (spicy, though often too oily for me), and Taiwan (my favorite use of seafood). The following are overrated:
|
||||
</p><p>
|
||||
Cantonese: surely the most overrated cuisine in China, and perhaps the most overrated cuisine in the world. I concede that dim sum is often a delight; and no lunch can be more simple or more satisfying than a few cuts of roast duck or pork layered on a bed of rice, accompanied by sprigs of greens and some gravy. But we’ve too long allowed Cantonese food to dominate the world’s conception of Chinese cuisine. The high-end dishes don’t come close to the refinement of Shanghai cuisine: chefs reveal contempt for themselves and their craft when they deep fry a lobster, as if it were a carnival food, and I’ve never understood the emphasis on shark fins and sea cucumbers. Please let’s not continue allowing Cantonese to be a default choice for business lunches, Shanghai is more fine.
|
||||
</p><p>
|
||||
Beijing’s imperial cuisine is the only Chinese cuisine that I consider to be dumb. It wasn’t until I moved to Beijing that I realized how many of the unfortunate facts of Chinese cooking are the creation of local traditions: the dreadful “brown sauce,” the excessive use of starch, and the compulsive need to fry. Peking duck is fine every once in a while, but it’s far too much fuss and expense for something of medium tastiness. There are so few redeeming dishes in imperial cuisine that I wonder if it has been yet another cruel trick pulled by the eunuchs to hoodwink the emperor, depriving him of culinary pleasures for sport.
|
||||
</p><p>
|
||||
Hotpot transcends regions now, so let’s treat it as its own category. Hotpot is a fun social activity to do with friends. It’s a way to display skill at the table, through the management of cooking a variety of foods. But it can never be any sort of culinary revelation. My worst nightmare is for hotpot restaurants to take over every retail restaurant space, so that our only choice is to line up to eat at them inside malls, forever.
|
||||
</p><p>
|
||||
Here is my four-step process for ordering success in China:
|
||||
</p><ol><li>Greens are usually the glories of the cuisine: order as many vegetables as there are people</li><li>If you will have a meat, consider the juiciness that pairs well with the starch: something saucy if you will eat with rice, or less saucy if you will have soup noodles</li><li>Order Yunnan mushrooms if they are on the menu</li><li>Fill out the rest with cold appetizers, they are never a bad idea</li></ol><p>
|
||||
***
|
||||
</p><p>
|
||||
Personal matters for last. 2020 was mostly a fine year, I didn’t have too many issues with it.
|
||||
</p><p>
|
||||
After a quick Italian holiday over lunar new year, I returned to Beijing on February 1st. At first I hesitated to fly back, today it looks like the best decision I made this year. It gave me the chance not only to observe the country as it faced its greatest challenge in decades, but also to enjoy normal life more quickly than most other places in the world. One can question the ultimate number of cases in China. But even if the totals were an order of magnitude higher, the reported trend that the country mostly stomped out the virus by April feels correct to all of us living here. Ever since that point, various cities have had to deal with flare ups (including Beijing again in June), but life has been a series of loosening restrictions. The last big milestone was the re-opening of cinemas in August. But well before then, the malls had been once again full and the smart restaurants difficult to book.
|
||||
</p><p>
|
||||
In early April, I wrote a <a rel="noreferrer noopener" href="https://nymag.com/intelligencer/2020/04/life-after-covid-the-view-from-beijing.html" target="_blank">feature</a> for New York Magazine’s Intelligencer on life in Beijing during the worst of the pandemic. There isn’t too much more I’d add, since life had already started to return to normal by then. I haven’t been able to visit the US at all this year, and as bad as things look now, I wonder if the virus will have a long-term impact to extend American strengths. There’s no question that the scientific establishment did a fantastic job, though from afar it doesn’t look like many other segments of society really distinguished themselves. But I wonder if this prolonged experience with the virus will shake loose a lot of the self-imposed paralysis in the US, thus inducing greater domestic change than in China, which dispatched the virus relatively quickly.
|
||||
</p><p>
|
||||
In 2019, I spent around two weeks a month out of the country on work travel. This year, my travel was domestic, and I’m glad to have been to visit six new cities: Qingdao, Suzhou, Hefei, Nanjing, Xi’an, and Changchun. Over the summer, everyone went to a handful of places: Hainan’s beaches, Yunnan’s villages, or the Sichuan mountainside. I didn’t go to these places, but had a memorable trip to Mount Changbai in the northeast, which offers <a href="https://twitter.com/danwwang/status/1338637625314463747" target="_blank" rel="noreferrer noopener">stark</a> and frigid alpine scenery. All things considered, Beijing has been one of the best places to be in the world this year, but its lack of nature made me dream of the big forests and wide rivers I grew with in Ontario and the US northeast. I don’t think that there’s anything quite like that in China, but I hope next to be able to see the mountains in Sichuan or the plains of Inner Mongolia.
|
||||
</p><p>
|
||||
The combination of less travel and more news events raised my productivity this year. I came close to doubling my work output, from already-high levels, because of a relentless pace of White House actions against Huawei, Tencent, ByteDance, SMIC, and other firms. I wanted to write more for Bloomberg this year, but managed only two columns: discussing the state of US <a href="https://www.bloomberg.com/opinion/articles/2020-05-07/why-american-manufacturing-can-t-handle-the-coronavirus" target="_blank" rel="noreferrer noopener">manufacturing</a> in May and why this time is <a href="https://www.bloomberg.com/opinion/articles/2020-12-18/u-s-sanctions-against-china-will-fuel-xi-jinping-s-tech-ambitions" target="_blank" rel="noreferrer noopener">different</a> for Chinese industrial policy in December. In terms of public writing, I’m happy to have contributed the piece to New York Magazine, and would like to try my hand at more feature writing in the future.
|
||||
</p><p>
|
||||
Like everyone else, I did a lot of Zoom meetings. I gave a presentation roughly at a rate of once a week, mostly to private audiences. My favorite public event was a <a href="https://www.youtube.com/watch?v=qs1PGD6AXPk" target="_blank" rel="noreferrer noopener">keynote</a> I gave for Asia Society Northern California, in which I presented on semiconductors and then moderated a panel that consisted of two technology and two policy experts. And I went on a series of podcasts: I think my best discussion was with the Economist’s Money Talks on China’s institutional <a href="https://www.economist.com/podcasts/2020/10/20/xinomics-a-new-economic-era-is-dawning-in-china" target="_blank" rel="noreferrer noopener">strengths</a>. One of the unexpected delights of this year, created by the norm of doing Zoom calls, was to hear from old friends, many of whom I haven’t seen in years or decades. I’m glad that this was a year that created this possibility for reconnection.
|
||||
</p><p>
|
||||
In the early months of the pandemic, I picked up a new skill: riding a bike. I’ve always been mortified to admit that I never properly knew how. With the encouragement of kind and patient friends, I’ve enjoyed cycling so much that it has become the primary way I get around Beijing. The city is good for cyclists, with its wide bicycle paths and flat roads. (Given the behavior of most drivers though, Beijing requires taking seriously the principle of safety first.) My favorite activity has become to cycle to the Forbidden City and back home, a nice hour-long ride that I would do after lunch. I’m still enjoying the feeling of gliding down a road on my own propulsion, which gives me a sense of slight unreality. That’s been good for thinking: I wrote significant chunks of this letter while riding down Beijing’s second and fourth ring roads.
|
||||
</p><p>
|
||||
This year marks my seventh of not drinking. I expect that I’m in the best shape of my life, given that, regular bike rides, occasional badminton sessions, and working out with my personal trainer three times a week. Still, I’m exhausted. That doesn’t mean it’s time to slow down. There are too many interesting things left to do.
|
||||
</p><figure><img loading="lazy" src="https://i1.wp.com/danwang.co/wp-content/uploads/2021/01/nasa-titan.jpg?fit=700%2C1044&ssl=1" alt="" srcset="https://i1.wp.com/danwang.co/wp-content/uploads/2021/01/nasa-titan.jpg?w=1000&ssl=1 1000w, https://i1.wp.com/danwang.co/wp-content/uploads/2021/01/nasa-titan.jpg?resize=465%2C694&ssl=1 465w, https://i1.wp.com/danwang.co/wp-content/uploads/2021/01/nasa-titan.jpg?resize=700%2C1044&ssl=1 700w, https://i1.wp.com/danwang.co/wp-content/uploads/2021/01/nasa-titan.jpg?resize=768%2C1146&ssl=1 768w, https://i1.wp.com/danwang.co/wp-content/uploads/2021/01/nasa-titan.jpg?resize=940%2C1402&ssl=1 940w, https://i1.wp.com/danwang.co/wp-content/uploads/2021/01/nasa-titan.jpg?resize=200%2C298&ssl=1 200w" sizes="(max-width: 1000px) 100vw, 1000px"/></figure><p>
|
||||
<em>Titan, a planet-sized moon of Saturn, has a thick atmosphere and liquid oceans. It and Europa—one of the moons of Jupiter, which might have warm liquid oceans—offer the best chances of discovering extraterrestrial life in our solar system. Credit:</em> <a href="https://www.jpl.nasa.gov/visions-of-the-future/" target="_blank" rel="noreferrer noopener"><em>JPL</em></a>
|
||||
</p><p>
|
||||
Here are a few questions on which I think we should all have a view:
|
||||
</p><p>
|
||||
Is the successful export of TikTok the start of a new trend or a one-off? The app has been an innovative Chinese creation that became a global success. Is it just the first example of many more successful products to come, or something that looks more like Three-Body Problem today: an exception, not the start of a trend.
|
||||
</p><p>
|
||||
Are there enduring advantages to being a producer-friendly economy? The west this year made a political decision to direct stimulus to consumers, while China offered minimal support to households and concentrated on helping production. Its implicit view is that production is more valuable and more difficult to stimulate than consumption. More generally, Chinese officials tend to be incredulous of US complaints of excessive subsidies to manufacturers; they tend to ask what the problem is, as if they’ve been accused of the sin of loving a child too much.
|
||||
</p><p>
|
||||
Will we recognize what broader Chinese success or failure will look like? Since reform and opening, the country has always looked like it was on the brink of some disaster, either economic, political, or financial. Meanwhile, it has avoided big crises as it improves various capabilities. If that’s still the story at the end of the next decade—a decent rate of growth, avoiding the worst crises, while facing tough challenges—should we deem that a success or a failure?
|
||||
</p><ul><li><h2>
|
||||
Subscribe to new posts
|
||||
</h2></li></ul><ol><li><p>
|
||||
see Anne-Marie Brady: Marketing Dictatorship: Propaganda and Thought Work in Contemporary China<a href="#note-1-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
中国特色社会主义有很多特点和特征,但最本质的特征是坚持中国共产党领导。http://www.qstheory.cn/dukan/qs/2020-07/15/c_1126234524.htm<a href="#note-2-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
For more, see Charles Fishman’s excellent One Giant Leap, which showed how NASA had to invent a thousand and one technologies to reach the moon <a href="#note-3-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
Sometimes translated as “rule of law”: 依法治国<a href="#note-4-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
see Dan Grover on the UI changes that Chinese apps made: http://dangrover.com/blog/2020/04/05/covid-in-ui.html<a href="#note-6-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
That’s a broad and unfair generalization, I know. This Economist leader offers a more nuanced view: https://www.economist.com/briefing/2020/08/15/xi-jinping-is-trying-to-remake-the-chinese-economy<a href="#note-7-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
This is my translation of 不忘初心、牢记使命. There are variations on the third line, I included one I’ve seen: 永远奋斗 <a href="#note-8-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
see this excellent discussion between Frederick Teiwes and Joseph Torigian https://omny.fm/shows/the-little-red-podcast/xi-dada-and-daddy-power-the-party-and-the-presiden<a href="#note-9-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
From Dialectical Materialism Is the Worldview and Methodology of Chinese Communists, 广大党员、干部理想信念坚定、干事创业精气神足,人民群众精神振奋、发愤图强,就可以创造出很多人间奇迹 http://www.qstheory.cn/dukan/qs/2018-12/31/c_1123923896.htm<a href="#note-10-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
Report to the 19th party congress: http://www.xinhuanet.com/english/download/Xi_Jinping’s_report_at_19th_CPC_National_Congress.pdf<a href="#note-11-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
see The Decadent Society<a href="#note-12-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
经济工作是中心工作,党的领导当然要在中心工作中得到充分体现,抓住了中心工作这个牛鼻子,其他工作就可以更好展开。http://www.qstheory.cn/dukan/qs/2020-07/15/c_1126234524.htm<a href="#note-13-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
see Frank Pieke’s Knowing China<a href="#note-14-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
see Dialectical Materialism Is the Worldview and Methodology of Chinese Communists 如果对矛盾熟视无睹,甚至回避、掩饰矛盾,在矛盾面前畏缩不前,坐看矛盾恶性转化,那就会积重难返,最后势必造成无法弥补的损失。 http://www.qstheory.cn/dukan/qs/2018-12/31/c_1123923896.htm<a href="#note-15-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
from the speech at the Third Plenary Session of the 19th Central Commission for Discipline Inspection<a href="#note-16-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
from Goals of the Aspiration and Mission Education Campaign, May 31 2019<a href="#note-17-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
http://www.chinafilm.gov.cn/chinafilm/contents/141/2533.shtml<a href="#note-18-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
Wang Hongsheng, a boss at Jinghai, admits to fretting about interruptions to chick supplies, even wondering if President Donald Trump might curb American exports. https://www.economist.com/china/2020/10/31/high-tech-chickens-are-a-case-study-of-why-self-reliance-is-so-hard<a href="#note-19-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
see this WSJ story https://www.wsj.com/articles/the-u-s-vs-china-the-high-cost-of-the-technology-cold-war-11603397438 and Doug Fuller’s claim on Tokyo Electron https://www.jhuapl.edu/assessing-us-china-technology-connections/publications<a href="#note-20-1569">↩</a>
|
||||
</p></li><li><p>
|
||||
This is admittedly a bit of my own fanciful translation of 必须看到,实体经济是基础,各种制造业不能丢,作为14亿人口的大国,粮食和实体产业要以自己为主,这一条绝对不能丢 http://www.qstheory.cn/dukan/qs/2020-10/31/c_1126680390.htm<a href="#note-21-1569">↩</a>
|
||||
</p></li></ol></div>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue