core(fr): prepare emulation utilities for shared use (#12375)
This commit is contained in:
Родитель
8c0ffa3b77
Коммит
f57ac8c57c
|
@ -8,7 +8,6 @@
|
|||
const Fetcher = require('./fetcher.js');
|
||||
const ExecutionContext = require('./driver/execution-context.js');
|
||||
const {waitForFullyLoaded, waitForFrameNavigated} = require('./driver/wait-for-condition.js');
|
||||
const emulation = require('../lib/emulation.js');
|
||||
const LHElement = require('../lib/lh-element.js');
|
||||
const LHError = require('../lib/lh-error.js');
|
||||
const NetworkRequest = require('../lib/network-request.js');
|
||||
|
@ -762,35 +761,6 @@ class Driver {
|
|||
await this.sendCommand('Debugger.setAsyncCallStackDepth', {maxDepth: 8});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LH.Config.Settings} settings
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async beginEmulation(settings) {
|
||||
await emulation.emulate(this, settings);
|
||||
await this.setThrottling(settings, {useThrottling: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LH.Config.Settings} settings
|
||||
* @param {{useThrottling?: boolean}} passConfig
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async setThrottling(settings, passConfig) {
|
||||
if (settings.throttlingMethod !== 'devtools') {
|
||||
return emulation.clearAllNetworkEmulation(this);
|
||||
}
|
||||
|
||||
const cpuPromise = passConfig.useThrottling ?
|
||||
emulation.enableCPUThrottling(this, settings.throttling) :
|
||||
emulation.disableCPUThrottling(this);
|
||||
const networkPromise = passConfig.useThrottling ?
|
||||
emulation.enableNetworkThrottling(this, settings.throttling) :
|
||||
emulation.clearAllNetworkEmulation(this);
|
||||
|
||||
await Promise.all([cpuPromise, networkPromise]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the network cache on disk and in memory.
|
||||
* @return {Promise<void>}
|
||||
|
|
|
@ -9,6 +9,7 @@ const log = require('lighthouse-logger');
|
|||
const LHError = require('../lib/lh-error.js');
|
||||
const NetworkAnalyzer = require('../lib/dependency-graph/simulator/network-analyzer.js');
|
||||
const NetworkRecorder = require('../lib/network-recorder.js');
|
||||
const emulation = require('../lib/emulation.js');
|
||||
const constants = require('../config/constants.js');
|
||||
const i18n = require('../lib/i18n/i18n.js');
|
||||
const URL = require('../lib/url-shim.js');
|
||||
|
@ -129,7 +130,7 @@ class GatherRunner {
|
|||
log.time(status);
|
||||
const resetStorage = !options.settings.disableStorageReset;
|
||||
await driver.assertNoSameOriginServiceWorkerClients(options.requestedUrl);
|
||||
await driver.beginEmulation(options.settings);
|
||||
await emulation.emulate(driver.defaultSession, options.settings);
|
||||
await driver.enableRuntimeEvents();
|
||||
await driver.enableAsyncStacks();
|
||||
await driver.cacheNatives();
|
||||
|
@ -344,8 +345,10 @@ class GatherRunner {
|
|||
const status = {msg: 'Setting up network for the pass trace', id: `lh:gather:setupPassNetwork`};
|
||||
log.time(status);
|
||||
|
||||
const session = passContext.driver.defaultSession;
|
||||
const passConfig = passContext.passConfig;
|
||||
await passContext.driver.setThrottling(passContext.settings, passConfig);
|
||||
if (passConfig.useThrottling) await emulation.throttle(session, passContext.settings);
|
||||
else await emulation.clearThrottling(session);
|
||||
|
||||
const blockedUrls = (passContext.passConfig.blockedUrlPatterns || [])
|
||||
.concat(passContext.settings.blockedUrlPatterns || []);
|
||||
|
@ -752,7 +755,7 @@ class GatherRunner {
|
|||
const loadData = await GatherRunner.endRecording(passContext);
|
||||
|
||||
// Disable throttling so the afterPass analysis isn't throttled
|
||||
await driver.setThrottling(passContext.settings, {useThrottling: false});
|
||||
await emulation.clearThrottling(driver.defaultSession);
|
||||
|
||||
// In case of load error, save log and trace with an error prefix, return no artifacts for this pass.
|
||||
const pageLoadError = GatherRunner.getPageLoadError(passContext, loadData, possibleNavError);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
/* globals window document getBoundingClientRect */
|
||||
|
||||
const Gatherer = require('./gatherer.js');
|
||||
const emulation = require('../../lib/emulation.js');
|
||||
const pageFunctions = require('../../lib/page-functions.js');
|
||||
|
||||
/** @typedef {import('../driver.js')} Driver */
|
||||
|
@ -147,7 +148,7 @@ class FullPageScreenshot extends Gatherer {
|
|||
} finally {
|
||||
// Revert resized page.
|
||||
if (lighthouseControlsEmulation) {
|
||||
await driver.beginEmulation(passContext.settings);
|
||||
await emulation.emulate(driver.defaultSession, passContext.settings);
|
||||
} else {
|
||||
// Best effort to reset emulation to what it was.
|
||||
// https://github.com/GoogleChrome/lighthouse/pull/10716#discussion_r428970681
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
*/
|
||||
'use strict';
|
||||
|
||||
/** @typedef {import('../gather/driver.js')} Driver */
|
||||
|
||||
const NO_THROTTLING_METRICS = {
|
||||
latency: 0,
|
||||
downloadThroughput: 0,
|
||||
|
@ -19,16 +17,15 @@ const NO_CPU_THROTTLE_METRICS = {
|
|||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Driver} driver
|
||||
* @param {LH.Gatherer.FRProtocolSession} session
|
||||
* @param {LH.Config.Settings} settings
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function emulate(driver, settings) {
|
||||
async function emulate(session, settings) {
|
||||
if (settings.emulatedUserAgent !== false) {
|
||||
// Network.enable must be called for UA overriding to work
|
||||
await driver.sendCommand('Network.enable');
|
||||
await driver.sendCommand('Network.setUserAgentOverride', {
|
||||
await session.sendCommand('Network.enable');
|
||||
await session.sendCommand('Network.setUserAgentOverride', {
|
||||
userAgent: /** @type {string} */ (settings.emulatedUserAgent),
|
||||
});
|
||||
}
|
||||
|
@ -36,20 +33,45 @@ async function emulate(driver, settings) {
|
|||
if (settings.screenEmulation.disabled !== true) {
|
||||
const {width, height, deviceScaleFactor, mobile} = settings.screenEmulation;
|
||||
const params = {width, height, deviceScaleFactor, mobile};
|
||||
await driver.sendCommand('Emulation.setDeviceMetricsOverride', params);
|
||||
await driver.sendCommand('Emulation.setTouchEmulationEnabled', {
|
||||
await session.sendCommand('Emulation.setDeviceMetricsOverride', params);
|
||||
await session.sendCommand('Emulation.setTouchEmulationEnabled', {
|
||||
enabled: params.mobile,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the throttling options specified in config settings, clearing existing network throttling if
|
||||
* throttlingMethod is not `devtools` (but not CPU throttling, suspected requirement of WPT-compat).
|
||||
*
|
||||
* @param {LH.Gatherer.FRProtocolSession} session
|
||||
* @param {LH.Config.Settings} settings
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function throttle(session, settings) {
|
||||
// TODO(FR-COMPAT): reconsider if this should be resetting anything
|
||||
if (settings.throttlingMethod !== 'devtools') return clearNetworkThrottling(session);
|
||||
|
||||
await Promise.all([
|
||||
enableNetworkThrottling(session, settings.throttling),
|
||||
enableCPUThrottling(session, settings.throttling),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Driver} driver
|
||||
* @param {LH.Gatherer.FRProtocolSession} session
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function clearThrottling(session) {
|
||||
await Promise.all([clearNetworkThrottling(session), clearCPUThrottling(session)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LH.Gatherer.FRProtocolSession} session
|
||||
* @param {Required<LH.ThrottlingSettings>} throttlingSettings
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
function enableNetworkThrottling(driver, throttlingSettings) {
|
||||
function enableNetworkThrottling(session, throttlingSettings) {
|
||||
/** @type {LH.Crdp.Network.EmulateNetworkConditionsRequest} */
|
||||
const conditions = {
|
||||
offline: false,
|
||||
|
@ -61,39 +83,41 @@ function enableNetworkThrottling(driver, throttlingSettings) {
|
|||
// DevTools expects throughput in bytes per second rather than kbps
|
||||
conditions.downloadThroughput = Math.floor(conditions.downloadThroughput * 1024 / 8);
|
||||
conditions.uploadThroughput = Math.floor(conditions.uploadThroughput * 1024 / 8);
|
||||
return driver.sendCommand('Network.emulateNetworkConditions', conditions);
|
||||
return session.sendCommand('Network.emulateNetworkConditions', conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Driver} driver
|
||||
* @param {LH.Gatherer.FRProtocolSession} session
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
function clearAllNetworkEmulation(driver) {
|
||||
return driver.sendCommand('Network.emulateNetworkConditions', NO_THROTTLING_METRICS);
|
||||
function clearNetworkThrottling(session) {
|
||||
return session.sendCommand('Network.emulateNetworkConditions', NO_THROTTLING_METRICS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Driver} driver
|
||||
* @param {LH.Gatherer.FRProtocolSession} session
|
||||
* @param {Required<LH.ThrottlingSettings>} throttlingSettings
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
function enableCPUThrottling(driver, throttlingSettings) {
|
||||
function enableCPUThrottling(session, throttlingSettings) {
|
||||
const rate = throttlingSettings.cpuSlowdownMultiplier;
|
||||
return driver.sendCommand('Emulation.setCPUThrottlingRate', {rate});
|
||||
return session.sendCommand('Emulation.setCPUThrottlingRate', {rate});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Driver} driver
|
||||
* @param {LH.Gatherer.FRProtocolSession} session
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
function disableCPUThrottling(driver) {
|
||||
return driver.sendCommand('Emulation.setCPUThrottlingRate', NO_CPU_THROTTLE_METRICS);
|
||||
function clearCPUThrottling(session) {
|
||||
return session.sendCommand('Emulation.setCPUThrottlingRate', NO_CPU_THROTTLE_METRICS);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
emulate,
|
||||
throttle,
|
||||
clearThrottling,
|
||||
enableNetworkThrottling,
|
||||
clearAllNetworkEmulation,
|
||||
clearNetworkThrottling,
|
||||
enableCPUThrottling,
|
||||
disableCPUThrottling,
|
||||
clearCPUThrottling,
|
||||
};
|
||||
|
|
|
@ -36,12 +36,6 @@ function makeFakeDriver({protocolGetVersionResponse}) {
|
|||
gotoURL(url) {
|
||||
return Promise.resolve({finalUrl: url, timedOut: false});
|
||||
},
|
||||
beginEmulation() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
setThrottling() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
dismissJavaScriptDialogs() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
|
|
@ -129,6 +129,17 @@ function resetDefaultMockResponses() {
|
|||
.mockResponse('ServiceWorker.enable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the emulation to its original implementation for testing emulation-sensitive logic.
|
||||
*/
|
||||
function restoreActualEmulation() {
|
||||
const actualEmulation = jest.requireActual('../../lib/emulation.js');
|
||||
const emulation = require('../../lib/emulation.js');
|
||||
emulation.emulate = actualEmulation.emulate;
|
||||
emulation.throttle = actualEmulation.throttle;
|
||||
emulation.clearThrottling = actualEmulation.clearThrottling;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error - connectionStub has a mocked version of sendCommand implemented in each test
|
||||
connectionStub = new Connection();
|
||||
|
@ -138,6 +149,11 @@ beforeEach(() => {
|
|||
};
|
||||
driver = new EmulationDriver(connectionStub);
|
||||
resetDefaultMockResponses();
|
||||
|
||||
const emulation = require('../../lib/emulation.js');
|
||||
emulation.emulate = jest.fn();
|
||||
emulation.throttle = jest.fn();
|
||||
emulation.clearThrottling = jest.fn();
|
||||
});
|
||||
|
||||
describe('GatherRunner', function() {
|
||||
|
@ -275,17 +291,14 @@ describe('GatherRunner', function() {
|
|||
});
|
||||
|
||||
it('sets up the driver to begin emulation when all flags are undefined', async () => {
|
||||
restoreActualEmulation();
|
||||
await GatherRunner.setupDriver(driver, {settings: getSettings('mobile')});
|
||||
|
||||
connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride');
|
||||
expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
|
||||
latency: 0, downloadThroughput: 0, uploadThroughput: 0, offline: false,
|
||||
});
|
||||
expect(() =>
|
||||
connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toThrow();
|
||||
});
|
||||
|
||||
it('applies the correct emulation given a particular formFactor', async () => {
|
||||
restoreActualEmulation();
|
||||
await GatherRunner.setupDriver(driver, {settings: getSettings('mobile')});
|
||||
expect(connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride'))
|
||||
.toMatchObject({mobile: true});
|
||||
|
@ -296,30 +309,6 @@ describe('GatherRunner', function() {
|
|||
.toMatchObject({mobile: false});
|
||||
});
|
||||
|
||||
it('sets throttling according to settings', async () => {
|
||||
await GatherRunner.setupDriver(driver, {
|
||||
settings: {
|
||||
formFactor: 'mobile',
|
||||
screenEmulation: constants.screenEmulationMetrics.mobile,
|
||||
throttlingMethod: 'devtools',
|
||||
throttling: {
|
||||
requestLatencyMs: 100,
|
||||
downloadThroughputKbps: 8,
|
||||
uploadThroughputKbps: 8,
|
||||
cpuSlowdownMultiplier: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride');
|
||||
expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
|
||||
latency: 100, downloadThroughput: 1024, uploadThroughput: 1024, offline: false,
|
||||
});
|
||||
expect(connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toEqual({
|
||||
rate: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('clears origin storage', () => {
|
||||
const asyncFunc = () => Promise.resolve();
|
||||
/** @type {Record<string, boolean>} */
|
||||
|
@ -334,8 +323,6 @@ describe('GatherRunner', function() {
|
|||
};
|
||||
const driver = {
|
||||
assertNoSameOriginServiceWorkerClients: asyncFunc,
|
||||
beginEmulation: asyncFunc,
|
||||
setThrottling: asyncFunc,
|
||||
dismissJavaScriptDialogs: asyncFunc,
|
||||
enableRuntimeEvents: asyncFunc,
|
||||
enableAsyncStacks: asyncFunc,
|
||||
|
@ -371,7 +358,6 @@ describe('GatherRunner', function() {
|
|||
beginTrace: asyncFunc,
|
||||
gotoURL: async () => ({}),
|
||||
cleanBrowserCaches: createCheck('calledCleanBrowserCaches'),
|
||||
setThrottling: asyncFunc,
|
||||
blockUrlPatterns: asyncFunc,
|
||||
setExtraHTTPHeaders: asyncFunc,
|
||||
endTrace: asyncFunc,
|
||||
|
@ -525,8 +511,6 @@ describe('GatherRunner', function() {
|
|||
};
|
||||
const driver = {
|
||||
assertNoSameOriginServiceWorkerClients: asyncFunc,
|
||||
beginEmulation: asyncFunc,
|
||||
setThrottling: asyncFunc,
|
||||
dismissJavaScriptDialogs: asyncFunc,
|
||||
enableRuntimeEvents: asyncFunc,
|
||||
enableAsyncStacks: asyncFunc,
|
||||
|
@ -548,6 +532,64 @@ describe('GatherRunner', function() {
|
|||
});
|
||||
});
|
||||
|
||||
it('sets throttling appropriately', async () => {
|
||||
restoreActualEmulation();
|
||||
await GatherRunner.setupPassNetwork({
|
||||
driver,
|
||||
settings: {
|
||||
formFactor: 'mobile',
|
||||
screenEmulation: constants.screenEmulationMetrics.mobile,
|
||||
throttlingMethod: 'devtools',
|
||||
throttling: {
|
||||
requestLatencyMs: 100,
|
||||
downloadThroughputKbps: 8,
|
||||
uploadThroughputKbps: 8,
|
||||
cpuSlowdownMultiplier: 2,
|
||||
},
|
||||
},
|
||||
passConfig: {
|
||||
useThrottling: true,
|
||||
gatherers: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
|
||||
latency: 100, downloadThroughput: 1024, uploadThroughput: 1024, offline: false,
|
||||
});
|
||||
expect(connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toEqual({
|
||||
rate: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('clears throttling when useThrottling=false', async () => {
|
||||
restoreActualEmulation();
|
||||
await GatherRunner.setupPassNetwork({
|
||||
driver,
|
||||
settings: {
|
||||
formFactor: 'mobile',
|
||||
screenEmulation: constants.screenEmulationMetrics.mobile,
|
||||
throttlingMethod: 'devtools',
|
||||
throttling: {
|
||||
requestLatencyMs: 100,
|
||||
downloadThroughputKbps: 8,
|
||||
uploadThroughputKbps: 8,
|
||||
cpuSlowdownMultiplier: 2,
|
||||
},
|
||||
},
|
||||
passConfig: {
|
||||
useThrottling: false,
|
||||
gatherers: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
|
||||
latency: 0, downloadThroughput: 0, uploadThroughput: 0, offline: false,
|
||||
});
|
||||
expect(connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toEqual({
|
||||
rate: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('tells the driver to block given URL patterns when blockedUrlPatterns is given', async () => {
|
||||
await GatherRunner.setupPassNetwork({
|
||||
driver,
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
/* eslint-env jest */
|
||||
|
||||
let mockEmulate = jest.fn();
|
||||
jest.mock('../../../lib/emulation.js', () => ({emulate: (...args) => mockEmulate(...args)}));
|
||||
|
||||
const FullPageScreenshotGatherer = require('../../../gather/gatherers/full-page-screenshot.js');
|
||||
|
||||
// Headless's default value is (1024 * 16), but this varies by device
|
||||
|
@ -16,6 +19,21 @@ const maxTextureSizeMock = 1024 * 8;
|
|||
* @param {{contentSize: {width: number, height: number}, screenSize: {width?: number, height?: number, dpr: number}, screenshotData: string[]}}
|
||||
*/
|
||||
function createMockDriver({contentSize, screenSize, screenshotData}) {
|
||||
const sendCommand = jest.fn().mockImplementation(method => {
|
||||
if (method === 'Page.getLayoutMetrics') {
|
||||
return {
|
||||
contentSize,
|
||||
// See comment within _takeScreenshot() implementation
|
||||
layoutViewport: {clientWidth: contentSize.width, clientHeight: contentSize.height},
|
||||
};
|
||||
}
|
||||
if (method === 'Page.captureScreenshot') {
|
||||
return {
|
||||
data: screenshotData && screenshotData.length ? screenshotData.shift() : 'abc',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
executionContext: {
|
||||
evaluate: async function(fn) {
|
||||
|
@ -40,25 +58,16 @@ function createMockDriver({contentSize, screenSize, screenshotData}) {
|
|||
}
|
||||
},
|
||||
},
|
||||
beginEmulation: jest.fn(),
|
||||
sendCommand: jest.fn().mockImplementation(method => {
|
||||
if (method === 'Page.getLayoutMetrics') {
|
||||
return {
|
||||
contentSize,
|
||||
// See comment within _takeScreenshot() implementation
|
||||
layoutViewport: {clientWidth: contentSize.width, clientHeight: contentSize.height},
|
||||
};
|
||||
}
|
||||
if (method === 'Page.captureScreenshot') {
|
||||
return {
|
||||
data: screenshotData && screenshotData.length ? screenshotData.shift() : 'abc',
|
||||
};
|
||||
}
|
||||
}),
|
||||
sendCommand,
|
||||
defaultSession: {sendCommand},
|
||||
};
|
||||
}
|
||||
|
||||
describe('FullPageScreenshot gatherer', () => {
|
||||
beforeEach(() => {
|
||||
mockEmulate = jest.fn();
|
||||
});
|
||||
|
||||
it('captures a full-page screenshot', async () => {
|
||||
const fpsGatherer = new FullPageScreenshotGatherer();
|
||||
const driver = createMockDriver({
|
||||
|
@ -113,8 +122,8 @@ describe('FullPageScreenshot gatherer', () => {
|
|||
await fpsGatherer.afterPass(passContext);
|
||||
|
||||
const expectedArgs = {formFactor: 'mobile', screenEmulation: {disabled: false, mobile: true}};
|
||||
expect(driver.beginEmulation).toHaveBeenCalledWith(expectedArgs);
|
||||
expect(driver.beginEmulation).toHaveBeenCalledTimes(1);
|
||||
expect(mockEmulate).toHaveBeenCalledTimes(1);
|
||||
expect(mockEmulate).toHaveBeenCalledWith(driver.defaultSession, expectedArgs);
|
||||
});
|
||||
|
||||
it('resets the emulation correctly when Lighthouse does not control it', async () => {
|
||||
|
|
|
@ -235,6 +235,11 @@ function makeMocksForGatherRunner() {
|
|||
jest.mock('../gather/gatherers/web-app-manifest.js', () => ({
|
||||
getWebAppManifest: async () => null,
|
||||
}));
|
||||
jest.mock('../lib/emulation.js', () => ({
|
||||
emulate: jest.fn(),
|
||||
throttle: jest.fn(),
|
||||
clearThrottling: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
Загрузка…
Ссылка в новой задаче